DEV Community

Werner Echezuría
Werner Echezuría

Posted on

10 1

Migrate to Actix-Web 2.0

I decided it was time for a version upgrade to Actix Web 2.0, so, in order to make it easier I migrated most of the endpoints to GraphQL.

GraphQL

I have to recognize I had my doubts about GraphQL, but after a few steps I took in this project, I recognize why it's growing in popularity. A few benefits:

  • I just need one endpoint and define through functions and methods what resource will be modified or added.
  • I don't need to define every detail, that's what GraphQL is for, the client will define what is the content that will be defined and what will be modified.
  • I don't need to define a standard, it's already established.

In order to make it work, I created one file for mutations and another for queries, like this:

src/graphql/mutation.rs:

use juniper::FieldResult;
use crate::models::Context;
use crate::models::sale::{Sale, NewSale, FullSale};
use crate::models::sale_product::NewSaleProducts;
use crate::models::price::NewPriceProductsToUpdate;
use crate::models::product::{FullProduct, Product, NewProduct};
use crate::models::sale_state::Event;
use crate::models::price::{NewPrice, Price};

pub struct Mutation;

#[juniper::object(
    Context = Context,
)]
impl Mutation {
    fn createSale(
        context: &Context,
        param_new_sale: NewSale,
        param_new_sale_products: NewSaleProducts,
    ) -> FieldResult<FullSale> {
        Sale::create_sale(context, param_new_sale, param_new_sale_products)
    }

    fn approveSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        Sale::set_state(context, sale_id, Event::Approve)
    }

    fn cancelSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform credit note or debit note
        Sale::set_state(context, sale_id, Event::Cancel)
    }

    fn paySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform collection
        Sale::set_state(context, sale_id, Event::Pay)
    }

    fn partiallyPaySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform collection
        Sale::set_state(context, sale_id, Event::PartiallyPay)
    }

    fn updateSale(
        context: &Context,
        param_sale: NewSale,
        param_sale_products: NewSaleProducts,
    ) -> FieldResult<FullSale> {
        Sale::update_sale(context, param_sale, param_sale_products)
    }

    fn destroySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        Sale::destroy_sale(context, sale_id)
    }

    fn createProduct(
        context: &Context,
        param_new_product: NewProduct,
        param_new_price_products: NewPriceProductsToUpdate,
    ) -> FieldResult<FullProduct> {
        Product::create_product(context, param_new_product, param_new_price_products)
    }

    fn updateProduct(
        context: &Context,
        param_product: NewProduct,
        param_price_products: NewPriceProductsToUpdate,
    ) -> FieldResult<FullProduct> {
        Product::update_product(context, param_product, param_price_products)
    }

    fn destroyProduct(context: &Context, product_id: i32) -> FieldResult<bool> {
        Product::destroy_product(context, product_id)
    }

    fn createPrice(context: &Context, new_price: NewPrice) -> FieldResult<Price> {
        Price::create(context, new_price)
    }

    fn updatePrice(context: &Context, edit_price: NewPrice) -> FieldResult<Price> {
        Price::update(context, edit_price)
    }

    fn destroyPrice(context: &Context, price_id: i32) -> FieldResult<bool> {
        Price::destroy(context, price_id)
    }
}

src/graphql/query.rs:

use juniper::FieldResult;
use crate::models::Context;
use crate::models::sale::{Sale, NewSale, ListSale, FullSale};
use crate::models::product::{Product, ListProduct, FullProduct};
use crate::models::price::{PriceList, Price};

pub struct Query;

#[juniper::object(
    Context = Context,
)]
impl Query {
    fn listSale(context: &Context, search: Option<NewSale>, limit: i32) -> FieldResult<ListSale> {
        Sale::list_sale(context, search, limit)
    }

    fn sale(context: &Context, sale_id: i32) -> FieldResult<FullSale> {
        Sale::sale(context, sale_id)
    }

    fn listProduct(context: &Context, search: String, limit: i32, rank: f64) -> FieldResult<ListProduct> {
        Product::list_product(context, search, limit, rank)
    }

    fn product(context: &Context, product_id: i32) -> FieldResult<FullProduct> {
        Product::product(context, product_id)
    }

    fn ListPrice(context: &Context) -> FieldResult<PriceList> {
        PriceList::list(context)
    }

    fn price(context: &Context, price_id: i32) -> FieldResult<Price> {
        Price::find(context, price_id)
    }

}

And another file for the schema:

src/graphql/schema.rs:

use crate::graphql::query::Query;
use crate::graphql::mutation::Mutation;

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

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

Actix Web 2.0

I think I like the new way to define endpoints in ActixWeb, it looks like Rocket, especially because of routes annotations. The big change was defined in src/graphql/mod.rs.

src/graphql/mod.rs:

pub mod query;
pub mod mutation;
pub mod schema;

use std::sync::Arc;
use actix_web::{web, Error, HttpResponse, post, get};
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;
use schema::Schema;

use crate::models::create_context;
use crate::handlers::LoggedUser;
use crate::db_connection::PgPool;
use crate::serde::ser::Error as SerdeError;

#[get("/graphiql")]
pub async fn graphiql() -> HttpResponse {
    let html = graphiql_source("http://127.0.0.1:8080/graphql");
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

#[post("/graphql")]
pub async fn graphql(
    st: web::Data<Arc<Schema>>,
    data: web::Json<GraphQLRequest>,
    user: LoggedUser,
    pool: web::Data<PgPool>
) -> Result<HttpResponse, Error> {
    let user =
        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(&st, &ctx);
            Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
        })
        .await?;
    Ok(HttpResponse::Ok()
        .content_type("application/json")
        .body(user))
}

There are other changes, more difficult that implies altering the integration tests, the more important one was the test server.

tests/sale_test.rs:

        let srv = RefCell::new(test_server(move || {
            HttpService::build()
                .h1(map_config(
                    App::new()
                        .wrap(
                            IdentityService::new(
                                CookieIdentityPolicy::new(dotenv!("SECRET_KEY").as_bytes())
                                    .domain("localhost")
                                    .name("mystorejwt")
                                    .path("/")
                                    .max_age(Duration::days(1).num_seconds())
                                    .secure(false)
                            )
                        )
                        .wrap(
                            Cors::new()
                                .allowed_origin("localhost")
                                .allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE"])
                                .allowed_headers(vec![header::AUTHORIZATION,
                                                    header::CONTENT_TYPE,
                                                    header::ACCEPT,
                                                    csrf_token_header.clone()])
                                .expose_headers(vec![csrf_token_header.clone()])
                                .max_age(3600)
                                .finish()
                        )
                        .data(
                            CsrfTokenGenerator::new(
                                dotenv!("CSRF_TOKEN_KEY").as_bytes().to_vec(),
                                Duration::hours(1)
                            )
                        )
                        .data(establish_connection())
                        .data(schema.clone())
                        .service(graphql)
                        .service(graphiql)
                        .service(::mystore_lib::handlers::authentication::login)
                        .service(::mystore_lib::handlers::authentication::logout)
                    ,  |_| AppConfig::default(),
                ))
                .tcp()
            }
        ));

If you want to take a look at the source code, you can do it through this link

PS. I'm openly looking for a new job (remote), Experience in Ruby on Rails and Rust enthusiast, :).

Image of Datadog

How to Diagram Your Cloud Architecture

Cloud architecture diagrams provide critical visibility into the resources in your environment and how they’re connected. In our latest eBook, AWS Solution Architects Jason Mimick and James Wenzel walk through best practices on how to build effective and professional diagrams.

Download the Free eBook

Top comments (1)

Collapse
 
sunsoftio profile image
Sunsoft-io

Hi Werner,

do you want to rewrite the UNDollar.org software with Rust and Actix?

Thanks...Yours...Willi, UNDollar.org

willi@rauffer.org

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay