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

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

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

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

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

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

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

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

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

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

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

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

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

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


    //other code section goes here

    [dependencies]
    rocket = {version = "0.5.0-rc.2", features = ["json"]}
    async-graphql = { version = "4.0", features = ["bson", "chrono"] }
    async-graphql-rocket = "4.0.0"
    serde = "1.0.136"
    dotenv = "0.15.0"

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

rocket = {version = "0.5.0-rc.2", features = ["json"]} — это основанный на Rust фреймворк для создания веб-приложений. Здесь также указывается требуемая версия и тип функций (json).

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

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

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

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

[dependencies.mongodb] — драйвер для подключения к MongoDB. Здесь также указывается необходимая версия и тип функции (Sync API).

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

    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 std::{env, io::Error};
    use mongodb::{
        bson::{doc, oid::ObjectId},
        sync::{Client, Collection, Database},
    };
    use crate::schemas::project_schema::{Owner, Project};

    pub struct DBMongo {
        db: Database,
    }

    impl DBMongo {
        pub 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).unwrap();
            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 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 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)
                .ok()
                .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 fn get_owners(&self) -> Result<Vec<Owner>, Error> {
            let col = DBMongo::col_helper::<Owner>(&self, "owner");
            let cursors = col
                .find(None, None)
                .ok()
                .expect("Error getting list of owners");
            let owners: Vec<Owner> = cursors.map(|doc| doc.unwrap()).collect();
            Ok(owners)
        }

        pub 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)
                .ok()
                .expect("Error getting owner's detail");
            Ok(owner_detail.unwrap())
        }

        //project logics
        pub 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)
                .ok()
                .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 fn get_projects(&self) -> Result<Vec<Project>, Error> {
            let col = DBMongo::col_helper::<Project>(&self, "project");
            let cursors = col
                .find(None, None)
                .ok()
                .expect("Error getting list of projects");
            let projects: Vec<Project> = cursors.map(|doc| doc.unwrap()).collect();
            Ok(projects)
        }

        pub 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)
                .ok()
                .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 без фильтра, чтобы она могла найти все документы в базе данных и вернуть список оптимальным образом, используя метод map() для цикла по списку владельцев и обработки ошибок.
  • Добавляет метод 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 без фильтра, чтобы она могла найти все документы в базе данных и оптимально вернуть список, используя метод map() для циклического просмотра списка проектов и обработки ошибок.
  • Добавляет метод 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).unwrap();
            Ok(owner)
        }

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

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

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

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

  • Импортирует необходимые зависимости
  • Создает Query struct с методами реализации, связанными с запросом к базе данных, используя соответствующие методы из логики базы данных
  • Создает 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 async_graphql::{
        http::{playground_source, GraphQLPlaygroundConfig},
        EmptySubscription, Schema,
    };
    use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse};
    use config::mongo::DBMongo;
    use handler::graphql_handler::{Mutation, ProjectSchema, Query};
    use rocket::{response::content, routes, State};

    #[rocket::get("/graphql?<query..>")]
    async fn graphql_query(schema: &State<ProjectSchema>, query: GraphQLQuery) -> GraphQLResponse {
        query.execute(schema).await
    }

    #[rocket::post("/graphql", data = "<request>", format = "application/json")]
    async fn graphql_mutation(
        schema: &State<ProjectSchema>,
        request: GraphQLRequest,
    ) -> GraphQLResponse {
        request.execute(schema).await
    }

    #[rocket::get("/")]
    async fn graphql_playground() -> content::RawHtml<String> {
        content::RawHtml(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
    }

    #[rocket::launch]
    fn rocket() -> _ {
        let db = DBMongo::init();
        let schema = Schema::build(Query, Mutation, EmptySubscription)
            .data(db)
            .finish();
        rocket::build().manage(schema).mount(
            "/",
            routes![graphql_query, graphql_mutation, graphql_playground],
        )
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

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

  • Импортирует необходимые зависимости
  • Создает функцию graphql_query с процедурным макросом get для указания маршрута GraphQL и использует тип ProjectSchema для выполнения методов, связанных с запросом к базе данных
  • Создает функцию graphql_mutation с процедурным макросом post для указания маршрута GraphQL и использует тип ProjectSchema для выполнения методов, связанных с модификацией базы данных
  • Создает функцию graphql_playground для создания GraphiQL; площадки GraphQL, к которой мы можем получить доступ через браузер.
  • Использует макрос #[rocket::launch] для запуска функции rocket, которая генерирует точку входа приложения и запускает сервер. Функция rocket также делает следующее:
    • Создает переменную db для установления соединения с MongoDB путем вызова метода init() и использует ее для построения данных GraphQL.
    • Создает приложение с помощью функции build, добавляет schema в state, и настраивает маршрут для включения graphql_query, graphql_mutation, и graphql_playground.

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

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

Затем перейдите по адресу http://127.0.0.1:8000 в веб-браузере.



Заключение

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

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

  • Async-GraphQL
  • Rocket
  • MongoDB Rust Driver
  • Построение REST API с помощью Rust и MongoDB
  • Интеграция Async-GraphQL Rocket
  • Serde (библиотека сериализации и десериализации)

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