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, :).

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

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 Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more