From 8783cc38f5dadb6a0b3541bfc82401c82a24ff09 Mon Sep 17 00:00:00 2001 From: Eugene Korir Date: Wed, 20 Dec 2023 15:44:22 +0300 Subject: [PATCH 1/8] Initial setup --- examples/web_app_graphql/Cargo.toml | 25 +++++++++ examples/web_app_graphql/diesel.toml | 9 ++++ examples/web_app_graphql/migrations/.keep | 0 .../down.sql | 6 +++ .../up.sql | 36 +++++++++++++ .../2023-12-20-105441_users/down.sql | 2 + .../migrations/2023-12-20-105441_users/up.sql | 7 +++ examples/web_app_graphql/src/lib.rs | 54 +++++++++++++++++++ examples/web_app_graphql/src/main.rs | 54 +++++++++++++++++++ 9 files changed, 193 insertions(+) create mode 100644 examples/web_app_graphql/Cargo.toml create mode 100644 examples/web_app_graphql/diesel.toml create mode 100644 examples/web_app_graphql/migrations/.keep create mode 100644 examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 examples/web_app_graphql/migrations/2023-12-20-105441_users/down.sql create mode 100644 examples/web_app_graphql/migrations/2023-12-20-105441_users/up.sql create mode 100644 examples/web_app_graphql/src/lib.rs create mode 100644 examples/web_app_graphql/src/main.rs diff --git a/examples/web_app_graphql/Cargo.toml b/examples/web_app_graphql/Cargo.toml new file mode 100644 index 00000000..014ed5cc --- /dev/null +++ b/examples/web_app_graphql/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "meilisearch-ex" +version = "0.1.0" +edition = "2021" +authors = ["Eugene Korir "] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-cors = "0.6.5" +actix-web = "4.4.0" +async-graphql = "6.0.11" +async-graphql-actix-web = "6.0.11" +diesel = { version = "2.1.4", features = ["postgres"] } +diesel-async = { version = "0.4.1", features = ["postgres", "deadpool"] } +diesel_migrations = "2.1.0" +dotenvy = "0.15.7" +env_logger = "0.10.1" +envy = "0.4.2" +futures = "0.3.29" +log = "0.4.20" +meilisearch-sdk = "0.24.3" +serde = { version = "1.0.192", features = ["derive"] } +serde_json = "1.0.108" +thiserror = "1.0.51" diff --git a/examples/web_app_graphql/diesel.toml b/examples/web_app_graphql/diesel.toml new file mode 100644 index 00000000..c028f4a6 --- /dev/null +++ b/examples/web_app_graphql/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId"] + +[migrations_directory] +dir = "migrations" diff --git a/examples/web_app_graphql/migrations/.keep b/examples/web_app_graphql/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/down.sql b/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 00000000..a9f52609 --- /dev/null +++ b/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/up.sql b/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 00000000..d68895b1 --- /dev/null +++ b/examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/examples/web_app_graphql/migrations/2023-12-20-105441_users/down.sql b/examples/web_app_graphql/migrations/2023-12-20-105441_users/down.sql new file mode 100644 index 00000000..f910f702 --- /dev/null +++ b/examples/web_app_graphql/migrations/2023-12-20-105441_users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table if exists users; \ No newline at end of file diff --git a/examples/web_app_graphql/migrations/2023-12-20-105441_users/up.sql b/examples/web_app_graphql/migrations/2023-12-20-105441_users/up.sql new file mode 100644 index 00000000..b5dacea3 --- /dev/null +++ b/examples/web_app_graphql/migrations/2023-12-20-105441_users/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +create table if not exists users( + id serial primary key, + first_name varchar not null, + last_name varchar not null, + email varchar not null unique +); \ No newline at end of file diff --git a/examples/web_app_graphql/src/lib.rs b/examples/web_app_graphql/src/lib.rs new file mode 100644 index 00000000..01bc3d52 --- /dev/null +++ b/examples/web_app_graphql/src/lib.rs @@ -0,0 +1,54 @@ +pub mod app_env_vars; +pub mod errors; +mod graphql_schema; +mod models; +mod schema; + +use actix_web::{web, HttpResponse, Result}; +use app_env_vars::AppEnvVars; +use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; +use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; +use diesel_async::{ + pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager}, + AsyncPgConnection, +}; +use errors::ApplicationError; +use graphql_schema::Query; +use meilisearch_sdk::Client as SearchClient; + +pub type ApplicationSchema = Schema; + +pub struct GraphQlData { + pub pool: Pool, + pub client: SearchClient, +} + +pub async fn index_graphiql() -> Result { + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(GraphiQLSource::build().endpoint("/").finish())) +} + +pub async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { + let req_inner = req.into_inner(); + + schema.execute(req_inner).await.into() +} + +pub fn build_schema(app_env_vars: &AppEnvVars) -> Result { + let client = SearchClient::new( + &app_env_vars.meilisearch_host, + Some(&app_env_vars.meilisearch_api_key), + ); + + let config = AsyncDieselConnectionManager::::new(&app_env_vars.database_url); + let pool = Pool::builder(config).build()?; + + let schema_data = GraphQlData { pool, client }; + + Ok( + Schema::build(Query::default(), EmptyMutation, EmptySubscription) + .data(schema_data) + .finish(), + ) +} diff --git a/examples/web_app_graphql/src/main.rs b/examples/web_app_graphql/src/main.rs new file mode 100644 index 00000000..6109d7c4 --- /dev/null +++ b/examples/web_app_graphql/src/main.rs @@ -0,0 +1,54 @@ +use actix_cors::Cors; +use actix_web::middleware::Logger; +use actix_web::web; +use actix_web::{guard, App, HttpServer}; +use diesel::migration::MigrationSource; +use diesel::{Connection, PgConnection}; +use diesel_migrations::FileBasedMigrations; +use meilisearch_ex::{build_schema, index, index_graphiql, app_env_vars::AppEnvVars, errors::ApplicationError}; + + +#[actix_web::main] +async fn main() -> Result<(), ApplicationError> { + let _ = dotenvy::dotenv(); + + let app_env_vars = envy::from_env::()?; + + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let mut db_connection = PgConnection::establish(&app_env_vars.database_url)?; + let mut migrations = FileBasedMigrations::from_path(&app_env_vars.migrations_dir_path)? + .migrations() + .unwrap(); + + migrations.sort_by_key(|m| m.name().to_string()); + + for migration in migrations { + migration.run(&mut db_connection).unwrap(); + } + + let schema = build_schema(&app_env_vars)?; + + println!("GraphiQL IDE: http://localhost:8081"); + + HttpServer::new(move || { + App::new() + .wrap(Logger::default()) + .wrap( + Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600) + .supports_credentials(), + ) + .app_data(web::Data::new(schema.clone())) + .service(web::resource("/").guard(guard::Post()).to(index)) + .service(web::resource("/").guard(guard::Get()).to(index_graphiql)) + }) + .bind("0.0.0.0:8081")? + .run() + .await?; + + Ok(()) +} From fde462acbc35cde67992e05197df38895e2785a6 Mon Sep 17 00:00:00 2001 From: Eugene Korir Date: Wed, 20 Dec 2023 15:45:39 +0300 Subject: [PATCH 2/8] Graphql schema and error handling --- examples/web_app_graphql/src/errors/mod.rs | 25 ++++++++ .../web_app_graphql/src/graphql_schema/mod.rs | 9 +++ .../src/graphql_schema/users/get_users.rs | 25 ++++++++ .../src/graphql_schema/users/mod.rs | 9 +++ .../src/graphql_schema/users/search.rs | 60 +++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 examples/web_app_graphql/src/errors/mod.rs create mode 100644 examples/web_app_graphql/src/graphql_schema/mod.rs create mode 100644 examples/web_app_graphql/src/graphql_schema/users/get_users.rs create mode 100644 examples/web_app_graphql/src/graphql_schema/users/mod.rs create mode 100644 examples/web_app_graphql/src/graphql_schema/users/search.rs diff --git a/examples/web_app_graphql/src/errors/mod.rs b/examples/web_app_graphql/src/errors/mod.rs new file mode 100644 index 00000000..a172f0ce --- /dev/null +++ b/examples/web_app_graphql/src/errors/mod.rs @@ -0,0 +1,25 @@ +use diesel::ConnectionError; +use diesel_async::pooled_connection::deadpool::{BuildError, PoolError}; +use diesel_migrations::MigrationError; +use serde_json::Error as SerdeError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ApplicationError { + #[error("Missing environment variable")] + Envy(#[from] envy::Error), + #[error("Input/Output error")] + Io(#[from] std::io::Error), + #[error("Database error")] + Diesel(#[from] diesel::result::Error), + #[error("Deadpool build error")] + DeadpoolBuild(#[from] BuildError), + #[error("Migration error")] + Migration(#[from] MigrationError), + #[error("Connection error")] + DieselConnection(#[from] ConnectionError), + #[error("Pool Error")] + Pool(#[from] PoolError), + #[error("Serde json error")] + SerDe(#[from] SerdeError), +} diff --git a/examples/web_app_graphql/src/graphql_schema/mod.rs b/examples/web_app_graphql/src/graphql_schema/mod.rs new file mode 100644 index 00000000..1183f12c --- /dev/null +++ b/examples/web_app_graphql/src/graphql_schema/mod.rs @@ -0,0 +1,9 @@ +use async_graphql::SimpleObject; +pub mod users; + +use users::UsersQuery; + +#[derive(Default, SimpleObject)] +pub struct Query { + users: UsersQuery, +} diff --git a/examples/web_app_graphql/src/graphql_schema/users/get_users.rs b/examples/web_app_graphql/src/graphql_schema/users/get_users.rs new file mode 100644 index 00000000..c4ee4efe --- /dev/null +++ b/examples/web_app_graphql/src/graphql_schema/users/get_users.rs @@ -0,0 +1,25 @@ +use async_graphql::{Context, Object, Result}; +use diesel_async::RunQueryDsl; + +use crate::{models::User, GraphQlData}; + +#[derive(Default)] +pub struct GetUsers; + +#[Object] +impl GetUsers { + pub async fn get_users(&self, ctx: &Context<'_>) -> Result> { + use crate::schema::users::dsl::users; + + let GraphQlData { pool, .. } = ctx.data().map_err(|e| { + log::error!("Failed to get app data: {:?}", e); + e + })?; + + let mut connection = pool.get().await?; + + let list_users = users.load::(&mut connection).await?; + + Ok(list_users) + } +} diff --git a/examples/web_app_graphql/src/graphql_schema/users/mod.rs b/examples/web_app_graphql/src/graphql_schema/users/mod.rs new file mode 100644 index 00000000..08f5b575 --- /dev/null +++ b/examples/web_app_graphql/src/graphql_schema/users/mod.rs @@ -0,0 +1,9 @@ +use async_graphql::MergedObject; +pub mod get_users; +pub mod search; + +use get_users::GetUsers; +use search::SearchUsers; + +#[derive(Default, MergedObject)] +pub struct UsersQuery(pub GetUsers, pub SearchUsers); diff --git a/examples/web_app_graphql/src/graphql_schema/users/search.rs b/examples/web_app_graphql/src/graphql_schema/users/search.rs new file mode 100644 index 00000000..1af37d94 --- /dev/null +++ b/examples/web_app_graphql/src/graphql_schema/users/search.rs @@ -0,0 +1,60 @@ +use async_graphql::{Context, Object, Result}; +use diesel_async::RunQueryDsl; +use meilisearch_sdk::search::{SearchQuery, SearchResults}; + +use crate::{models::User, GraphQlData}; + +#[derive(Default)] +pub struct SearchUsers; + +#[Object] +impl SearchUsers { + async fn search(&self, ctx: &Context<'_>, query_string: String) -> Result> { + use crate::schema::users::dsl::users; + + let app_data = ctx.data::().map_err(|e| { + log::error!("Failed to get app data: {:?}", e); + e + })?; + + let mut connection = app_data.pool.get().await?; + + let list_users = users.load::(&mut connection).await?; + + match app_data.client.get_index("users").await { + Ok(index) => { + index.add_documents(&list_users, Some("id")).await?; + } + Err(_) => { + let task = app_data.client.create_index("users", Some("id")).await?; + let task = task + .wait_for_completion(&app_data.client, None, None) + .await?; + let index = task.try_make_index(&app_data.client).unwrap(); + + index.add_documents(&list_users, Some("id")).await?; + } + } + + let index = app_data.client.get_index("users").await?; + + let query = SearchQuery::new(&index).with_query(&query_string).build(); + + let results: SearchResults = index.execute_query(&query).await?; + + log::error!("{:#?}", results.hits); + + let search_results: Vec = results + .hits + .into_iter() + .map(|hit| User { + id: hit.result.id, + email: hit.result.email, + first_name: hit.result.first_name, + last_name: hit.result.last_name, + }) + .collect(); + + Ok(search_results) + } +} From 3c01675eff3157d3818babcffa2c7d92b62425cd Mon Sep 17 00:00:00 2001 From: Eugene Korir Date: Wed, 20 Dec 2023 15:46:38 +0300 Subject: [PATCH 3/8] Added Readme and app environment variables --- examples/web_app_graphql/.env.example.txt | 4 ++ examples/web_app_graphql/.gitignore | 4 ++ examples/web_app_graphql/Readme.md | 43 ++++++++++++++++++++ examples/web_app_graphql/src/app_env_vars.rs | 9 ++++ examples/web_app_graphql/src/models.rs | 14 +++++++ examples/web_app_graphql/src/schema.rs | 10 +++++ 6 files changed, 84 insertions(+) create mode 100644 examples/web_app_graphql/.env.example.txt create mode 100644 examples/web_app_graphql/.gitignore create mode 100644 examples/web_app_graphql/Readme.md create mode 100644 examples/web_app_graphql/src/app_env_vars.rs create mode 100644 examples/web_app_graphql/src/models.rs create mode 100644 examples/web_app_graphql/src/schema.rs diff --git a/examples/web_app_graphql/.env.example.txt b/examples/web_app_graphql/.env.example.txt new file mode 100644 index 00000000..9bc1ad78 --- /dev/null +++ b/examples/web_app_graphql/.env.example.txt @@ -0,0 +1,4 @@ +DATABASE_URL=postgres://[username]:[password]@localhost/[database_name] +MEILISEARCH_HOST=http://localhost:7700 +MEILISEARCH_API_KEY=[your-master-key] +MIGRATIONS_DIR_PATH=migrations \ No newline at end of file diff --git a/examples/web_app_graphql/.gitignore b/examples/web_app_graphql/.gitignore new file mode 100644 index 00000000..2966ec73 --- /dev/null +++ b/examples/web_app_graphql/.gitignore @@ -0,0 +1,4 @@ +/target +/Cargo.lock + +.env \ No newline at end of file diff --git a/examples/web_app_graphql/Readme.md b/examples/web_app_graphql/Readme.md new file mode 100644 index 00000000..e62ac618 --- /dev/null +++ b/examples/web_app_graphql/Readme.md @@ -0,0 +1,43 @@ +# Meilisearch example with graphql using `diesel`, `async_graphql` and `postgres` + +## Contents + +Setting up a graphql server using `async_graphql` and `actix-web` + +Using `diesel` to query the database + +Using `meilisearch-sdk` to search for records that match a given criteria + +## Running the example + +The meilisearch server needs to be running. You can run it by the command below + +```bash +meilisearch --master-key +``` + +Then you can run the application by simply running + +```bash +cargo run --release +``` + +The above command will display a link to your running instance and you can simply proceed by clicking the link or navigating to your browser. + +### Running the resolvers + +On your browser, you will see a graphql playground in which you can use to run some queries + +You can use the `searchUsers` query as follows: + +```gpl +query { + users{ + search(queryString: "Eugene"){ + lastName + firstName + email + } + } +} +``` diff --git a/examples/web_app_graphql/src/app_env_vars.rs b/examples/web_app_graphql/src/app_env_vars.rs new file mode 100644 index 00000000..da835fa0 --- /dev/null +++ b/examples/web_app_graphql/src/app_env_vars.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone)] +pub struct AppEnvVars { + pub meilisearch_api_key: String, + pub meilisearch_host: String, + pub database_url: String, + pub migrations_dir_path: String, +} diff --git a/examples/web_app_graphql/src/models.rs b/examples/web_app_graphql/src/models.rs new file mode 100644 index 00000000..0b4c9494 --- /dev/null +++ b/examples/web_app_graphql/src/models.rs @@ -0,0 +1,14 @@ +use async_graphql::SimpleObject; +use diesel::{Queryable, Selectable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::users; + +#[derive(SimpleObject, Deserialize, Serialize, Queryable, Selectable, Debug)] +#[diesel(table_name = users)] +pub struct User { + pub id: i32, + pub first_name: String, + pub last_name: String, + pub email: String, +} diff --git a/examples/web_app_graphql/src/schema.rs b/examples/web_app_graphql/src/schema.rs new file mode 100644 index 00000000..8955b674 --- /dev/null +++ b/examples/web_app_graphql/src/schema.rs @@ -0,0 +1,10 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + users (id) { + id -> Int4, + first_name -> Varchar, + last_name -> Varchar, + email -> Varchar, + } +} From 56217ef69d309143d6b4575c51502b60a4adf6d7 Mon Sep 17 00:00:00 2001 From: Eugene Korir Date: Wed, 20 Dec 2023 16:05:00 +0300 Subject: [PATCH 4/8] Comments depicting what the code does --- examples/web_app_graphql/src/app_env_vars.rs | 1 + .../src/graphql_schema/users/get_users.rs | 1 + .../src/graphql_schema/users/mod.rs | 1 + .../src/graphql_schema/users/search.rs | 24 ++++++++++--------- examples/web_app_graphql/src/lib.rs | 2 ++ examples/web_app_graphql/src/main.rs | 7 ++++-- examples/web_app_graphql/src/models.rs | 1 + 7 files changed, 24 insertions(+), 13 deletions(-) diff --git a/examples/web_app_graphql/src/app_env_vars.rs b/examples/web_app_graphql/src/app_env_vars.rs index da835fa0..72999ae2 100644 --- a/examples/web_app_graphql/src/app_env_vars.rs +++ b/examples/web_app_graphql/src/app_env_vars.rs @@ -1,5 +1,6 @@ use serde::Deserialize; +//Environment variables required for the app to run #[derive(Deserialize, Debug, Clone)] pub struct AppEnvVars { pub meilisearch_api_key: String, diff --git a/examples/web_app_graphql/src/graphql_schema/users/get_users.rs b/examples/web_app_graphql/src/graphql_schema/users/get_users.rs index c4ee4efe..fe685ef0 100644 --- a/examples/web_app_graphql/src/graphql_schema/users/get_users.rs +++ b/examples/web_app_graphql/src/graphql_schema/users/get_users.rs @@ -8,6 +8,7 @@ pub struct GetUsers; #[Object] impl GetUsers { + //Resolver for querying the database for user records pub async fn get_users(&self, ctx: &Context<'_>) -> Result> { use crate::schema::users::dsl::users; diff --git a/examples/web_app_graphql/src/graphql_schema/users/mod.rs b/examples/web_app_graphql/src/graphql_schema/users/mod.rs index 08f5b575..189392dc 100644 --- a/examples/web_app_graphql/src/graphql_schema/users/mod.rs +++ b/examples/web_app_graphql/src/graphql_schema/users/mod.rs @@ -5,5 +5,6 @@ pub mod search; use get_users::GetUsers; use search::SearchUsers; +//Combines user queries into one struct #[derive(Default, MergedObject)] pub struct UsersQuery(pub GetUsers, pub SearchUsers); diff --git a/examples/web_app_graphql/src/graphql_schema/users/search.rs b/examples/web_app_graphql/src/graphql_schema/users/search.rs index 1af37d94..51751578 100644 --- a/examples/web_app_graphql/src/graphql_schema/users/search.rs +++ b/examples/web_app_graphql/src/graphql_schema/users/search.rs @@ -12,38 +12,40 @@ impl SearchUsers { async fn search(&self, ctx: &Context<'_>, query_string: String) -> Result> { use crate::schema::users::dsl::users; - let app_data = ctx.data::().map_err(|e| { + let GraphQlData { pool, client } = ctx.data().map_err(|e| { log::error!("Failed to get app data: {:?}", e); e })?; - let mut connection = app_data.pool.get().await?; + let mut connection = pool.get().await?; let list_users = users.load::(&mut connection).await?; - match app_data.client.get_index("users").await { + match client.get_index("users").await { + //If getting the index is successful, we add documents to it Ok(index) => { index.add_documents(&list_users, Some("id")).await?; } + + //If getting the index fails, we create it and then add documents to the new index Err(_) => { - let task = app_data.client.create_index("users", Some("id")).await?; - let task = task - .wait_for_completion(&app_data.client, None, None) - .await?; - let index = task.try_make_index(&app_data.client).unwrap(); + let task = client.create_index("users", Some("id")).await?; + let task = task.wait_for_completion(client, None, None).await?; + let index = task.try_make_index(client).unwrap(); index.add_documents(&list_users, Some("id")).await?; } } - let index = app_data.client.get_index("users").await?; + let index = client.get_index("users").await?; + //We build the query let query = SearchQuery::new(&index).with_query(&query_string).build(); let results: SearchResults = index.execute_query(&query).await?; - log::error!("{:#?}", results.hits); - + //Tranform the results into a type that implements OutputType + //Required for return types to implement this trait let search_results: Vec = results .hits .into_iter() diff --git a/examples/web_app_graphql/src/lib.rs b/examples/web_app_graphql/src/lib.rs index 01bc3d52..c95ab0e0 100644 --- a/examples/web_app_graphql/src/lib.rs +++ b/examples/web_app_graphql/src/lib.rs @@ -18,6 +18,7 @@ use meilisearch_sdk::Client as SearchClient; pub type ApplicationSchema = Schema; +//Represents application data passed to graphql resolvers pub struct GraphQlData { pub pool: Pool, pub client: SearchClient, @@ -35,6 +36,7 @@ pub async fn index(schema: web::Data, req: GraphQLRequest) -> schema.execute(req_inner).await.into() } +//We build the graphql schema and any data required to be passed to all resolvers pub fn build_schema(app_env_vars: &AppEnvVars) -> Result { let client = SearchClient::new( &app_env_vars.meilisearch_host, diff --git a/examples/web_app_graphql/src/main.rs b/examples/web_app_graphql/src/main.rs index 6109d7c4..f7d218f1 100644 --- a/examples/web_app_graphql/src/main.rs +++ b/examples/web_app_graphql/src/main.rs @@ -5,8 +5,9 @@ use actix_web::{guard, App, HttpServer}; use diesel::migration::MigrationSource; use diesel::{Connection, PgConnection}; use diesel_migrations::FileBasedMigrations; -use meilisearch_ex::{build_schema, index, index_graphiql, app_env_vars::AppEnvVars, errors::ApplicationError}; - +use meilisearch_ex::{ + app_env_vars::AppEnvVars, build_schema, errors::ApplicationError, index, index_graphiql, +}; #[actix_web::main] async fn main() -> Result<(), ApplicationError> { @@ -16,6 +17,7 @@ async fn main() -> Result<(), ApplicationError> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + //Run migrations on app start let mut db_connection = PgConnection::establish(&app_env_vars.database_url)?; let mut migrations = FileBasedMigrations::from_path(&app_env_vars.migrations_dir_path)? .migrations() @@ -42,6 +44,7 @@ async fn main() -> Result<(), ApplicationError> { .max_age(3600) .supports_credentials(), ) + //Add schema to application `Data` extractor .app_data(web::Data::new(schema.clone())) .service(web::resource("/").guard(guard::Post()).to(index)) .service(web::resource("/").guard(guard::Get()).to(index_graphiql)) diff --git a/examples/web_app_graphql/src/models.rs b/examples/web_app_graphql/src/models.rs index 0b4c9494..21c99e2c 100644 --- a/examples/web_app_graphql/src/models.rs +++ b/examples/web_app_graphql/src/models.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::schema::users; +//Struct that corresponds to our database structure for users table #[derive(SimpleObject, Deserialize, Serialize, Queryable, Selectable, Debug)] #[diesel(table_name = users)] pub struct User { From 2efa72a3a930e71b3758584ad819b856fc80b709 Mon Sep 17 00:00:00 2001 From: Eugene Korir Date: Thu, 21 Dec 2023 16:08:35 +0300 Subject: [PATCH 5/8] Added function for signing up --- .../graphql_schema/users/mutation/add_user.rs | 68 +++++++++++++++++++ .../src/graphql_schema/users/mutation/mod.rs | 8 +++ 2 files changed, 76 insertions(+) create mode 100644 examples/web_app_graphql/src/graphql_schema/users/mutation/add_user.rs create mode 100644 examples/web_app_graphql/src/graphql_schema/users/mutation/mod.rs diff --git a/examples/web_app_graphql/src/graphql_schema/users/mutation/add_user.rs b/examples/web_app_graphql/src/graphql_schema/users/mutation/add_user.rs new file mode 100644 index 00000000..ff10ee1b --- /dev/null +++ b/examples/web_app_graphql/src/graphql_schema/users/mutation/add_user.rs @@ -0,0 +1,68 @@ +use async_graphql::{Context, InputObject, Object, Result}; +use diesel_async::RunQueryDsl; +use validator::Validate; + +use crate::{ + models::{NewUser, User}, + validate_input, GraphQlData, +}; + +#[derive(Default)] +pub struct AddUser; + +#[derive(InputObject, Validate)] +pub struct IAddUser { + #[validate(length(min = 1))] + pub first_name: String, + #[validate(length(min = 1))] + pub last_name: String, + #[validate(email)] + pub email: String, +} + +#[Object] +impl AddUser { + ///Resolver for creating a new user and storing that data in the database + /// + /// The mutation can be run as follows + /// ```gpl + /// mutation AddUser{ + /// users { + /// signup(input: {firstName: "",lastName: "",email: ""}){ + /// id + /// firstName + /// lastName + /// email + /// } + /// } + /// } + pub async fn signup(&self, ctx: &Context<'_>, input: IAddUser) -> Result { + validate_input(&input)?; + + use crate::schema::users::dsl::users; + + let GraphQlData { pool, .. } = ctx.data().map_err(|e| { + log::error!("Failed to get app data: {:?}", e); + e + })?; + + let mut connection = pool.get().await?; + + let value = NewUser { + first_name: input.first_name, + last_name: input.last_name, + email: input.email, + }; + + let result = diesel::insert_into(users) + .values(&value) + .get_result::(&mut connection) + .await + .map_err(|e| { + log::error!("Could not create new user: {:#?}", e); + e + })?; + + Ok(result) + } +} diff --git a/examples/web_app_graphql/src/graphql_schema/users/mutation/mod.rs b/examples/web_app_graphql/src/graphql_schema/users/mutation/mod.rs new file mode 100644 index 00000000..d1910348 --- /dev/null +++ b/examples/web_app_graphql/src/graphql_schema/users/mutation/mod.rs @@ -0,0 +1,8 @@ +pub mod add_user; + +use add_user::AddUser; +use async_graphql::MergedObject; + +//Combines user queries into one struct +#[derive(Default, MergedObject)] +pub struct UsersMut(pub AddUser); From c552ad03b151c9598f177cbd44347028f3dce5df Mon Sep 17 00:00:00 2001 From: Eugene Korir Date: Thu, 21 Dec 2023 16:09:40 +0300 Subject: [PATCH 6/8] Code refactor --- examples/web_app_graphql/Cargo.toml | 1 + .../web_app_graphql/src/graphql_schema/mod.rs | 8 +++++- .../src/graphql_schema/users/mod.rs | 12 ++------- .../users/{ => query}/get_users.rs | 0 .../src/graphql_schema/users/query/mod.rs | 10 +++++++ .../users/{ => query}/search.rs | 0 examples/web_app_graphql/src/lib.rs | 26 ++++++++++++++----- examples/web_app_graphql/src/models.rs | 10 ++++++- 8 files changed, 49 insertions(+), 18 deletions(-) rename examples/web_app_graphql/src/graphql_schema/users/{ => query}/get_users.rs (100%) create mode 100644 examples/web_app_graphql/src/graphql_schema/users/query/mod.rs rename examples/web_app_graphql/src/graphql_schema/users/{ => query}/search.rs (100%) diff --git a/examples/web_app_graphql/Cargo.toml b/examples/web_app_graphql/Cargo.toml index 014ed5cc..04156299 100644 --- a/examples/web_app_graphql/Cargo.toml +++ b/examples/web_app_graphql/Cargo.toml @@ -23,3 +23,4 @@ meilisearch-sdk = "0.24.3" serde = { version = "1.0.192", features = ["derive"] } serde_json = "1.0.108" thiserror = "1.0.51" +validator = { version = "0.16.1", features = ["derive"] } diff --git a/examples/web_app_graphql/src/graphql_schema/mod.rs b/examples/web_app_graphql/src/graphql_schema/mod.rs index 1183f12c..e78fe6bc 100644 --- a/examples/web_app_graphql/src/graphql_schema/mod.rs +++ b/examples/web_app_graphql/src/graphql_schema/mod.rs @@ -1,9 +1,15 @@ use async_graphql::SimpleObject; pub mod users; -use users::UsersQuery; +use users::mutation::UsersMut; +use users::query::UsersQuery; #[derive(Default, SimpleObject)] pub struct Query { users: UsersQuery, } + +#[derive(Default, SimpleObject)] +pub struct Mutation { + users: UsersMut, +} diff --git a/examples/web_app_graphql/src/graphql_schema/users/mod.rs b/examples/web_app_graphql/src/graphql_schema/users/mod.rs index 189392dc..90886b5e 100644 --- a/examples/web_app_graphql/src/graphql_schema/users/mod.rs +++ b/examples/web_app_graphql/src/graphql_schema/users/mod.rs @@ -1,10 +1,2 @@ -use async_graphql::MergedObject; -pub mod get_users; -pub mod search; - -use get_users::GetUsers; -use search::SearchUsers; - -//Combines user queries into one struct -#[derive(Default, MergedObject)] -pub struct UsersQuery(pub GetUsers, pub SearchUsers); +pub mod mutation; +pub mod query; diff --git a/examples/web_app_graphql/src/graphql_schema/users/get_users.rs b/examples/web_app_graphql/src/graphql_schema/users/query/get_users.rs similarity index 100% rename from examples/web_app_graphql/src/graphql_schema/users/get_users.rs rename to examples/web_app_graphql/src/graphql_schema/users/query/get_users.rs diff --git a/examples/web_app_graphql/src/graphql_schema/users/query/mod.rs b/examples/web_app_graphql/src/graphql_schema/users/query/mod.rs new file mode 100644 index 00000000..f5ed29e0 --- /dev/null +++ b/examples/web_app_graphql/src/graphql_schema/users/query/mod.rs @@ -0,0 +1,10 @@ +pub mod get_users; +pub mod search; + +use async_graphql::MergedObject; +use get_users::GetUsers; +use search::SearchUsers; + +//Combines user queries into one struct +#[derive(Default, MergedObject)] +pub struct UsersQuery(pub GetUsers, pub SearchUsers); diff --git a/examples/web_app_graphql/src/graphql_schema/users/search.rs b/examples/web_app_graphql/src/graphql_schema/users/query/search.rs similarity index 100% rename from examples/web_app_graphql/src/graphql_schema/users/search.rs rename to examples/web_app_graphql/src/graphql_schema/users/query/search.rs diff --git a/examples/web_app_graphql/src/lib.rs b/examples/web_app_graphql/src/lib.rs index c95ab0e0..bda57d8c 100644 --- a/examples/web_app_graphql/src/lib.rs +++ b/examples/web_app_graphql/src/lib.rs @@ -6,19 +6,22 @@ mod schema; use actix_web::{web, HttpResponse, Result}; use app_env_vars::AppEnvVars; -use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; +use async_graphql::{ + http::GraphiQLSource, EmptySubscription, Error, Result as GraphqlResult, Schema, +}; use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; use diesel_async::{ pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager}, AsyncPgConnection, }; use errors::ApplicationError; -use graphql_schema::Query; +use graphql_schema::{Mutation, Query}; use meilisearch_sdk::Client as SearchClient; +use validator::Validate; -pub type ApplicationSchema = Schema; +pub type ApplicationSchema = Schema; -//Represents application data passed to graphql resolvers +/// Represents application data passed to graphql resolvers pub struct GraphQlData { pub pool: Pool, pub client: SearchClient, @@ -36,7 +39,7 @@ pub async fn index(schema: web::Data, req: GraphQLRequest) -> schema.execute(req_inner).await.into() } -//We build the graphql schema and any data required to be passed to all resolvers +/// We build the graphql schema and any data required to be passed to all resolvers pub fn build_schema(app_env_vars: &AppEnvVars) -> Result { let client = SearchClient::new( &app_env_vars.meilisearch_host, @@ -49,8 +52,19 @@ pub fn build_schema(app_env_vars: &AppEnvVars) -> Result(input: &T) -> GraphqlResult<()> { + if let Err(e) = input.validate() { + log::error!("Validation error: {}", e); + let err = serde_json::to_string(&e).unwrap(); + let err = Error::from(err); + return Err(err); + } + Ok(()) +} diff --git a/examples/web_app_graphql/src/models.rs b/examples/web_app_graphql/src/models.rs index 21c99e2c..c3e4a06d 100644 --- a/examples/web_app_graphql/src/models.rs +++ b/examples/web_app_graphql/src/models.rs @@ -1,5 +1,5 @@ use async_graphql::SimpleObject; -use diesel::{Queryable, Selectable}; +use diesel::{prelude::Insertable, Queryable, Selectable}; use serde::{Deserialize, Serialize}; use crate::schema::users; @@ -13,3 +13,11 @@ pub struct User { pub last_name: String, pub email: String, } + +#[derive(Insertable, Debug)] +#[diesel(table_name = users)] +pub struct NewUser { + pub first_name: String, + pub last_name: String, + pub email: String, +} From 35263f3c05b10b800d9c3b805800839f246034e7 Mon Sep 17 00:00:00 2001 From: Eugene Korir <62384233+korir248@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:18:19 +0300 Subject: [PATCH 7/8] Rename Readme.md to README.md --- examples/web_app_graphql/{Readme.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/web_app_graphql/{Readme.md => README.md} (100%) diff --git a/examples/web_app_graphql/Readme.md b/examples/web_app_graphql/README.md similarity index 100% rename from examples/web_app_graphql/Readme.md rename to examples/web_app_graphql/README.md From da83801cf278da454bffa19ee980e0abbe65f216 Mon Sep 17 00:00:00 2001 From: Eugene Korir <62384233+korir248@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:32:16 +0300 Subject: [PATCH 8/8] Update README.md --- examples/web_app_graphql/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/web_app_graphql/README.md b/examples/web_app_graphql/README.md index e62ac618..b4b6803b 100644 --- a/examples/web_app_graphql/README.md +++ b/examples/web_app_graphql/README.md @@ -41,3 +41,20 @@ query { } } ``` + +### Errors + +Incase you run into the following error: + +```bash += note: ld: library not found for -lpq + clang: error: linker command failed with exit code 1 (use -v to see invocation) +``` + +Run: + +```bash +sudo apt install libpq-dev +``` + +This should fix the error