Создание конечной точки управления проектами на базе GraphQL в Rust и MongoDB — веб-версия Actix

GraphQL — это язык запросов для чтения и манипулирования данными для API. Его приоритетом является предоставление клиентам или серверам точных данных, обеспечивая гибкий и интуитивно понятный синтаксис для описания таких данных.

По сравнению с традиционным REST API, GraphQL предоставляет систему типов для описания схем данных и, в свою очередь, дает потребителям API возможность исследовать и запрашивать необходимые данные с помощью одной конечной точки.

В этом посте мы рассмотрим создание приложения для управления проектами на Rust с использованием библиотеки Async-graphql и MongoDB. В конце этого руководства мы узнаем, как создать конечную точку GraphQL, поддерживающую чтение и манипулирование данными управления проектами, и сохранить наши данные с помощью MongoDB.
Репозиторий GitHub можно найти здесь.

Предварительные условия

Для полного понимания концепций, представленных в этом руководстве, необходим опыт работы с Rust. Опыт работы с MongoDB не является обязательным условием, но его желательно иметь.

Нам также понадобится следующее:

  • Базовые знания GraphQL
  • Учетная запись MongoDB для размещения базы данных. Регистрация совершенно бесплатна

Давайте писать код

Начало работы

Для начала работы нам нужно перейти в нужную директорию и выполнить в терминале следующую команду

    cargo new project-mngt-rust-graphql-actix && cd project-mngt-rust-graphql-actix
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта команда создает проект Rust под названием project-mngt-rust-graphql-actix и переходит в каталог проекта.

Далее мы устанавливаем необходимые зависимости, изменяя секцию [dependencies] файла Cargo.toml, как показано ниже:

    //other code section goes here

    [dependencies]
    actix-web = "4"
    async-graphql = { version = "4.0", features = ["bson", "chrono"] }
    async-graphql-actix-web = "4.0"
    serde = "1.0.136"
    dotenv = "0.15.0"
    futures = "0.3"

    [dependencies.mongodb]
    version = "2.2.0"
Вход в полноэкранный режим Выход из полноэкранного режима

actix-web = "4" — это основанный на Rust фреймворк для создания веб-приложений.

async-graphql = { version = "4.0", features = ["bson", "chrono"] } — это серверная библиотека для построения GraphQL в Rust. Она также включает bson и chrono.

async-graphql-actix-web = "4.0" — это библиотека, которая помогает интегрировать async-grapql с actix web.

serde = "1.0.136" — это фреймворк для сериализации и десериализации структур данных Rust. Например, преобразование структур Rust в JSON.

dotenv = "0.15.0" — библиотека для управления переменными окружения.

futures = "0.3" — библиотека для асинхронного программирования в Rust.

[dependencies.mongodb] — драйвер для подключения к MongoDB. В нем также указана необходимая версия.

Нам нужно выполнить приведенную ниже команду для установки зависимостей:

    cargo build
Войти в полноэкранный режим Выйти из полноэкранного режима

Система модулей в Rust

Модули — это как структуры папок в нашем приложении; они упрощают управление зависимостями.

Для этого нам нужно перейти в папку src и создать папки config, handler и schemas с соответствующим им файлом mod.rs для управления видимостью.

config предназначен для модульного построения конфигурационных файлов.

handler предназначен для модуляции логики GraphQL.

schemas предназначен для модулизации схем GraphQL.

Добавление ссылки на модули
Чтобы использовать код в модулях, нам нужно объявить их как модуль и импортировать в файл main.rs.

    //add modules
    mod config;
    mod handler;
    mod schemas;

    fn main() {
        println!("Hello, world!");
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Настройка MongoDB

После этого нам нужно войти или зарегистрироваться в нашей учетной записи MongoDB. Щелкните на выпадающем меню проекта и нажмите на кнопку New Project.

Введите projectMngt в качестве имени проекта, нажмите Next и нажмите Create Project.


Нажмите на кнопку Создать базу данных

Выберите Shared в качестве типа базы данных.

Нажмите кнопку Создать, чтобы настроить кластер. Настройка может занять некоторое время.

Далее нам нужно создать пользователя для внешнего доступа к базе данных, введя имя пользователя, пароль и нажав на кнопку Создать пользователя. Нам также нужно добавить наш IP-адрес для безопасного подключения к базе данных, нажав на кнопку Add My Current IP Address. Затем нажмите на Finish и Close, чтобы сохранить изменения.


После сохранения изменений мы должны увидеть экран Database Deployments, как показано ниже:

Подключение нашего приложения к MongoDB

После завершения конфигурации нам необходимо подключить наше приложение к созданной базе данных. Для этого нажмите на кнопку Connect

Нажмите на Connect your application, измените Driver на Rust и Version, как показано ниже. Затем нажмите на значок копирования, чтобы скопировать строку подключения.


Настройка переменной среды
Далее мы должны изменить скопированную строку подключения с паролем пользователя, который мы создали ранее, и изменить имя базы данных. Для этого сначала нужно создать файл .env в корневом каталоге, и в этот файл добавить фрагмент, приведенный ниже:

    MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority
Войти в полноэкранный режим Выйти из полноэкранного режима

Образец правильно заполненной строки подключения приведен ниже:

   MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5ahghkf.mongodb.net/projectMngt?retryWrites=true&w=majority
Вход в полноэкранный режим Выход из полноэкранного режима

Создание конечных точек GraphQL

После настройки нам необходимо создать схему для представления данных нашего приложения. Для этого нам нужно перейти в папку schemas, в ней создать файл project_schema.rs и добавить фрагмент ниже:

    use async_graphql::{Enum, InputObject, SimpleObject};
    use mongodb::bson::oid::ObjectId;
    use serde::{Deserialize, Serialize};

    //owner schema
    #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
    pub struct Owner {
        #[serde(skip_serializing_if = "Option::is_none")]
        pub _id: Option<ObjectId>,
        pub name: String,
        pub email: String,
        pub phone: String,
    }

    #[derive(InputObject)]
    pub struct CreateOwner {
        pub name: String,
        pub email: String,
        pub phone: String,
    }

    #[derive(InputObject)]
    pub struct FetchOwner {
        pub _id: String,
    }

    //project schema
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Enum)]
    pub enum Status {
        NotStarted,
        InProgress,
        Completed,
    }

    #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
    pub struct Project {
        #[serde(skip_serializing_if = "Option::is_none")]
        pub _id: Option<ObjectId>,
        pub owner_id: String,
        pub name: String,
        pub description: String,
        pub status: Status,
    }

    #[derive(InputObject)]
    pub struct CreateProject {
        pub owner_id: String,
        pub name: String,
        pub description: String,
        pub status: Status,
    }

    #[derive(InputObject)]
    pub struct FetchProject {
        pub _id: String,
    }
Вход в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше фрагмент делает следующее:

  • Импортирует необходимые зависимости
  • Использует макрос derive для создания поддержки реализации для Owner, CreateOwner, FetchOwner, Status, Project, CreateProject и FetchProject. Сниппет также использует процедурный макрос из библиотеки serde и async-graphql для сериализации/десериализации и преобразования структур Rust в схему GraphQL.

Далее мы должны зарегистрировать файл project_schema.rs как часть модуля schemas. Для этого откройте файл mod.rs в папке schemas и добавьте приведенный ниже фрагмент:

    pub mod project_schema;
Вход в полноэкранный режим Выйти из полноэкранного режима

Логика работы с базой данных
Когда схема полностью настроена и доступна для потребления, мы можем создать логику базы данных, которая будет выполнять следующие действия:

  • Создать владельца проекта
  • Получить всех владельцев
  • Получить одного владельца
  • Создать проект
  • Получить все проекты
  • Получить один проект

Для этого сначала нужно перейти в папку config, а в ней создать файл mongo.rs и добавить фрагмент ниже:

    use dotenv::dotenv;
    use futures::TryStreamExt;
    use std::{env, io::Error};
    use mongodb::{
        bson::{doc, oid::ObjectId},
        Client, Collection, Database,
    };
    use crate::schemas::project_schema::{Owner, Project};

    pub struct DBMongo {
        db: Database,
    }

    impl DBMongo {
        pub async fn init() -> Self {
            dotenv().ok();
            let uri = match env::var("MONGOURI") {
                Ok(v) => v.to_string(),
                Err(_) => format!("Error loading env variable"),
            };
            let client = Client::with_uri_str(uri)
                .await
                .expect("error connecting to database");
            let db = client.database("projectMngt");
            DBMongo { db }
        }

        fn col_helper<T>(data_source: &Self, collection_name: &str) -> Collection<T> {
            data_source.db.collection(collection_name)
        }
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше фрагмент делает следующее:

  • Импортирует необходимые зависимости
  • Создает структуру DBMongo с полем db для доступа к базе данных MongoDB
  • Создает блок реализации, который добавляет методы к структуре DBMongo
  • Добавляет метод init в блок реализации для загрузки переменной окружения, создает соединение с базой данных и возвращает экземпляр DBMongo struct
  • Добавляет метод col_helper; вспомогательную функцию для создания коллекции MongoDB

Далее нам нужно добавить оставшиеся методы в реализацию DBMongo для обслуживания операций управления проектом:

    //imports goes here

    pub struct DBMongo {
        db: Database,
    }

    impl DBMongo {
        pub async fn init() -> Self {
            //init code goes here
        }

        fn col_helper<T>(data_source: &Self, collection_name: &str) -> Collection<T> {
            data_source.db.collection(collection_name)
        }

        //Owners logic
        pub async fn create_owner(&self, new_owner: Owner) -> Result<Owner, Error> {
            let new_doc = Owner {
                _id: None,
                name: new_owner.name.clone(),
                email: new_owner.email.clone(),
                phone: new_owner.phone.clone(),
            };
            let col = DBMongo::col_helper::<Owner>(&self, "owner");
            let data = col
                .insert_one(new_doc, None)
                .await
                .expect("Error creating owner");
            let new_owner = Owner {
                _id: data.inserted_id.as_object_id(),
                name: new_owner.name.clone(),
                email: new_owner.email.clone(),
                phone: new_owner.phone.clone(),
            };
            Ok(new_owner)
        }

        pub async fn get_owners(&self) -> Result<Vec<Owner>, Error> {
            let col = DBMongo::col_helper::<Owner>(&self, "owner");
            let mut cursors = col
                .find(None, None)
                .await
                .expect("Error getting list of owners");
            let mut owners: Vec<Owner> = Vec::new();
            while let Some(owner) = cursors
                .try_next()
                .await
                .expect("Error mapping through cursor")
            {
                owners.push(owner)
            }
            Ok(owners)
        }

        pub async fn single_owner(&self, id: &String) -> Result<Owner, Error> {
            let obj_id = ObjectId::parse_str(id).unwrap();
            let filter = doc! {"_id": obj_id};
            let col = DBMongo::col_helper::<Owner>(&self, "owner");
            let owner_detail = col
                .find_one(filter, None)
                .await
                .expect("Error getting owner's detail");
            Ok(owner_detail.unwrap())
        }

        //project logics
        pub async fn create_project(&self, new_project: Project) -> Result<Project, Error> {
            let new_doc = Project {
                _id: None,
                owner_id: new_project.owner_id.clone(),
                name: new_project.name.clone(),
                description: new_project.description.clone(),
                status: new_project.status.clone(),
            };
            let col = DBMongo::col_helper::<Project>(&self, "project");
            let data = col
                .insert_one(new_doc, None)
                .await
                .expect("Error creating project");
            let new_project = Project {
                _id: data.inserted_id.as_object_id(),
                owner_id: new_project.owner_id.clone(),
                name: new_project.name.clone(),
                description: new_project.description.clone(),
                status: new_project.status.clone(),
            };
            Ok(new_project)
        }

        pub async fn get_projects(&self) -> Result<Vec<Project>, Error> {
            let col = DBMongo::col_helper::<Project>(&self, "project");
            let mut cursors = col
                .find(None, None)
                .await
                .expect("Error getting list of projects");
            let mut projects: Vec<Project> = Vec::new();
            while let Some(project) = cursors
                .try_next()
                .await
                .expect("Error mapping through cursor")
            {
                projects.push(project)
            }
            Ok(projects)
        }

        pub async fn single_project(&self, id: &String) -> Result<Project, Error> {
            let obj_id = ObjectId::parse_str(id).unwrap();
            let filter = doc! {"_id": obj_id};
            let col = DBMongo::col_helper::<Project>(&self, "project");
            let project_detail = col
                .find_one(filter, None)
                .await
                .expect("Error getting project's detail");
            Ok(project_detail.unwrap())
        }
    }
Войти в полноэкранный режим Выход из полноэкранного режима

Приведенный выше фрагмент делает следующее:

  • Добавляет метод create_owner, который принимает в качестве параметров self и new_owner и возвращает созданного владельца или ошибку. Внутри метода мы создали новый документ, используя структуру Owner. Затем мы использовали метод col_helper для создания новой коллекции и обратились к функции insert_one для создания нового владельца и обработки ошибок. Наконец, мы вернули информацию о созданном владельце.
  • Добавляет метод get_owners, который принимает в качестве параметров self и возвращает список владельцев или ошибку. Внутри метода мы используем метод col_helper для создания новой коллекции и доступа к функции find без фильтра, чтобы она могла найти все документы в базе данных и вернуть список оптимальным образом, используя метод try_next() для перебора списка владельцев и обработки ошибок.
  • Добавляет метод single_owner, который принимает self и id в качестве параметров и возвращает данные о владельце или ошибку. Внутри метода мы преобразовали id в ObjectId и использовали его в качестве фильтра для получения соответствующего документа. Затем мы используем метод col_helper для создания новой коллекции и обращаемся к функции find_one из коллекции для получения сведений о владельце и обработки ошибок.
  • Добавляет метод create_project, который принимает в качестве параметров self и new_project и возвращает созданный проект или ошибку. Внутри метода мы создали новый документ, используя структуру Project. Затем мы использовали метод col_helper для создания новой коллекции и обратились к функции insert_one для создания нового проекта и обработки ошибок. Наконец, мы вернули информацию о созданном проекте
  • Добавляет метод get_projects, который принимает в качестве параметров self и возвращает список проектов или ошибку. Внутри метода мы используем метод col_helper для создания новой коллекции и обращаемся к функции find без фильтра, чтобы она могла найти все документы в базе данных и вернуть список оптимальным образом, используя метод try_next() для цикла по списку проектов и обработки ошибок.
  • Добавляет метод single_project, который принимает в качестве параметров self и id и возвращает деталь проекта или ошибку. Внутри метода мы преобразовали id в ObjectId и использовали его в качестве фильтра для получения соответствующего документа. Затем мы используем метод col_helper для создания новой коллекции и обращения к функции find_one из коллекции для получения подробной информации о проекте и обработки ошибок.

Наконец, мы должны зарегистрировать файл mongo.rs как часть модуля config. Для этого откройте файл mod.rs в папке config и добавьте приведенный ниже фрагмент:

    pub mod mongo;
Войти в полноэкранный режим Выйти из полноэкранного режима

Обработчики GraphQL
Разобравшись с логикой базы данных, мы можем начать использовать их для создания обработчиков GraphQL. Для этого сначала нужно перейти в папку handler, а в ней создать файл graphql_handler.rs и добавить в него приведенный ниже фрагмент:

    use crate::{
        config::mongo::DBMongo,
        schemas::project_schema::{
            CreateOwner, CreateProject, FetchOwner, FetchProject, Owner, Project,
        },
    };
    use async_graphql::{Context, EmptySubscription, FieldResult, Object, Schema};

    pub struct Query;

    #[Object(extends)]
    impl Query {
        //owners query
        async fn owner(&self, ctx: &Context<'_>, input: FetchOwner) -> FieldResult<Owner> {
            let db = &ctx.data_unchecked::<DBMongo>();
            let owner = db.single_owner(&input._id).await.unwrap();
            Ok(owner)
        }

        async fn get_owners(&self, ctx: &Context<'_>) -> FieldResult<Vec<Owner>> {
            let db = &ctx.data_unchecked::<DBMongo>();
            let owners = db.get_owners().await.unwrap();
            Ok(owners)
        }

        //projects query
        async fn project(&self, ctx: &Context<'_>, input: FetchProject) -> FieldResult<Project> {
            let db = &ctx.data_unchecked::<DBMongo>();
            let project = db.single_project(&input._id).await.unwrap();
            Ok(project)
        }

        async fn get_projects(&self, ctx: &Context<'_>) -> FieldResult<Vec<Project>> {
            let db = &ctx.data_unchecked::<DBMongo>();
            let projects = db.get_projects().await.unwrap();
            Ok(projects)
        }
    }

    pub struct Mutation;

    #[Object]
    impl Mutation {
        //owner mutation
        async fn create_owner(&self, ctx: &Context<'_>, input: CreateOwner) -> FieldResult<Owner> {
            let db = &ctx.data_unchecked::<DBMongo>();
            let new_owner = Owner {
                _id: None,
                email: input.email,
                name: input.name,
                phone: input.phone,
            };
            let owner = db.create_owner(new_owner).await.unwrap();
            Ok(owner)
        }

        async fn create_project(
            &self,
            ctx: &Context<'_>,
            input: CreateProject,
        ) -> FieldResult<Project> {
            let db = &ctx.data_unchecked::<DBMongo>();
            let new_project = Project {
                _id: None,
                owner_id: input.owner_id,
                name: input.name,
                description: input.description,
                status: input.status,
            };
            let project = db.create_project(new_project).await.unwrap();
            Ok(project)
        }
    }

    pub type ProjectSchema = Schema<Query, Mutation, EmptySubscription>;
Вход в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше фрагмент делает следующее:

  • Импортирует необходимые зависимости
  • Создает структуру Query с методами реализации, связанными с запросом к базе данных, используя соответствующие методы из логики базы данных
  • Создает Mutation struct с методами реализации, связанными с модификацией базы данных, используя соответствующие методы из логики базы данных.
  • Создает тип ProjectSchema для построения того, как наш GraphQL использует Query struct, Mutation struct и EmptySubscription, поскольку у нас нет подписок.

Создание сервера GraphQL
Наконец, мы можем начать создавать наш GraphQL-сервер, интегрируя ProjectSchema и MongoDB с Actix web. Для этого нам нужно перейти к файлу main.rs и изменить его, как показано ниже:

    mod config;
    mod handler;
    mod schemas;

    //add 
    use actix_web::{
        guard,
        web::{self, Data},
        App, HttpResponse, HttpServer,
    };
    use async_graphql::{
        http::{playground_source, GraphQLPlaygroundConfig},
        EmptySubscription, Schema,
    };
    use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
    use config::mongo::DBMongo;
    use handler::graphql_handler::{Mutation, ProjectSchema, Query};


    //graphql entry
    async fn index(schema: Data<ProjectSchema>, req: GraphQLRequest) -> GraphQLResponse {
        schema.execute(req.into_inner()).await.into()
    }

    async fn graphql_playground() -> HttpResponse {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(playground_source(GraphQLPlaygroundConfig::new("/")))
    }

    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        //connect to the data source
        let db = DBMongo::init().await;
        let schema_data = Schema::build(Query, Mutation, EmptySubscription)
            .data(db)
            .finish();
        HttpServer::new(move || {
            App::new()
                .app_data(Data::new(schema_data.clone()))
                .service(web::resource("/").guard(guard::Post()).to(index))
                .service(
                    web::resource("/")
                        .guard(guard::Get())
                        .to(graphql_playground),
                )
        })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше фрагмент делает следующее:

  • Импортирует необходимые зависимости
  • Создает функцию index, которая использует тип ProjectSchema для создания сервера GraphQL
  • Создает функцию graphql_playground для создания GraphiQL; площадки GraphQL, к которой мы можем получить доступ через браузер.
  • Использует макрос #[actix_web::main] для асинхронного запуска функции main в среде выполнения actix. Функция main также делает следующее:
    • Создает переменную db для установления соединения с MongoDB путем вызова метода init() и использует ее для построения данных GraphQL.
    • Создает новый сервер, используя структуру HttpServer, которая использует закрытие для обслуживания входящих запросов, используя экземпляр App, который принимает данные GraphQL, добавляет службу Post для управления всеми входящими запросами GraphQL и метод Get для рендеринга игровой площадки GraphiQL
    • Настройте сервер на асинхронный запуск и обработку HTTP-запросов на localhost:8080.

После этого мы можем протестировать наше приложение, выполнив приведенную ниже команду в терминале.

    cargo run
Войти в полноэкранный режим Выйдите из полноэкранного режима

Затем перейдите на 127.0.0.1:8080 в веб-браузере.



Заключение

В этой статье мы рассмотрели, как модульно оформить приложение Rust, создать сервер GraphQL и сохранить наши данные с помощью MongoDB.

Эти ресурсы могут быть полезны:

  • Async-GraphQL
  • Actix web
  • Драйвер MongoDB Rust
  • Интеграция Async-GraphQL Actix
  • Создание REST API с помощью Rust и MongoDB
  • Serde (библиотека сериализации и десериализации)

Оцените статью
devanswers.ru
Добавить комментарий