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, :).
Top comments (1)
Hi Werner,
do you want to rewrite the UNDollar.org software with Rust and Actix?
Thanks...Yours...Willi, UNDollar.org
willi@rauffer.org