실용적인 Rust 웹 개발 - GraphQL

73680 단어 rustactixwwebgraphql
공식homepage에 따르면 GraphQL은 API용 쿼리 언어이자 기존 데이터로 이러한 쿼리를 수행하기 위한 런타임입니다. GraphQL은 API의 데이터에 대한 완전하고 이해하기 쉬운 설명을 제공하고, 클라이언트가 필요한 것을 정확하게 요청할 수 있는 능력을 제공하며, 시간이 지남에 따라 API를 더 쉽게 발전시키고 강력한 개발자 도구를 활성화합니다.

GraphQL의 한 가지 장점은 유연성입니다. 하나의 쿼리에서 시간이 지남에 따라 코드를 쉽게 유지 관리하고 서버와 클라이언트 간의 더 쉬운 통신을 허용하는 데 필요한 모든 것을 얻을 수 있습니다.

Juniper은 GraphQL 서버를 생성할 수 있는 크레이트이며 우리 프로젝트에서 사용할 것입니다.

온라인 상점을 계속 진행하겠습니다. 사이트에서 판매를 제어해야 하므로 GraphQL 쿼리를 수신할 판매 모듈을 만들어 보겠습니다.
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
)

그런 다음 모든 쿼리를 수신할 엔드포인트가 필요합니다. 이것은 사후 요청이 될 것입니다.
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(&st, &ctx);
        Ok::<_, serde_json::error::Error>(serde_json::to_string(&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))
        )

데이터를 읽는 응답을 출력하려면 쿼리가 필요하지만 상태를 수정해야 하는 경우 변형이 필요하므로 판매 모듈에 두 리소스를 모두 추가해 보겠습니다.
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::<diesel::pg::Pg>();

        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: &Context, search: Option<NewSale>, limit: i32) 
        -> FieldResult<ListSale> {
            use diesel::{ QueryDsl, RunQueryDsl, ExpressionMethods, GroupedBy };
            use crate::models::sale_product::SaleProduct;
            let conn: &PgConnection = &context.conn;
            let query = Sale::searching_records(search);

            let query_sales: Vec<Sale> =
                query
                    .filter(sales::dsl::user_id.eq(context.user_id))
                    .limit(limit.into())
                    .load::<Sale>(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::<(Product, SaleProduct)>(conn)?;

            let query_sale_products = 
                SaleProduct::belonging_to(&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::<(SaleProduct, Product)>(conn)?
                    .grouped_by(&query_sales);

            let tuple_full_sale: Vec<(Sale, Vec<(SaleProduct, Product)>)> = 
                query_sales
                    .into_iter()
                    .zip(query_sale_products)
                    .collect::<Vec<(Sale, Vec<(SaleProduct, Product)>)>>();

            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: &Context, sale_id: i32) -> FieldResult<FullSale> {
        use diesel::{ ExpressionMethods, QueryDsl, RunQueryDsl };

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

        let sale_products = 
            SaleProduct::belonging_to(&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::<(SaleProduct, Product)>(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: &Context, param_new_sale: NewSale, param_new_sale_products: NewSaleProducts) 
        -> FieldResult<FullSale> {
            use diesel::{ RunQueryDsl, Connection, QueryDsl };

            let conn: &PgConnection = &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::<Sale>(conn)?;

                let sale_products: Result<Vec<FullSaleProduct>, _> =
                    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::<SaleProduct>(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: &Context, param_sale: NewSale, param_sale_products: NewSaleProducts) 
        -> FieldResult<FullSale> {
            use diesel::QueryDsl;
            use diesel::RunQueryDsl;
            use diesel::ExpressionMethods;
            use diesel::Connection;
            use crate::schema::sales::dsl;

            let conn: &PgConnection = &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(&param_sale)
                        .get_result::<Sale>(conn)?;

                let sale_products: Result<Vec<FullSaleProduct>, _> =
                    param_sale_products.data.into_iter().map (|param_sale_product| {
                        let sale_product =
                            diesel::update(schema::sale_products::table)
                                .set(&param_sale_product.sale_product)
                                .get_result::<SaleProduct>(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: &Context, sale_id: i32) 
        -> FieldResult<i32> {
            use diesel::QueryDsl;
            use diesel::RunQueryDsl;
            use diesel::ExpressionMethods;
            use crate::schema::sales::dsl;

            let conn: &PgConnection = &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)}
} 

보시다시피 비즈니스 로직은 REST 엔드포인트와 매우 유사합니다. 변경할 수 있는 것은 데이터를 쿼리한 다음 create_schema 함수를 사용하여 스키마를 내보내는 것입니다.
src/main.rs :

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

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

이제 데이터를 가져오고 변형을 수행하는 방법은 무엇입니까? 그런 다음 몇 가지 테스트를 작성하고 모든 것이 예상대로 작동하는지 확인해야 합니다.
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", "");


전체 소스 코드here를 볼 수 있습니다.

좋은 웹페이지 즐겨찾기