diff --git a/examples/rocket_okapi_example/Cargo.toml b/examples/rocket_okapi_example/Cargo.toml new file mode 100644 index 000000000..87dfde6f3 --- /dev/null +++ b/examples/rocket_okapi_example/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sea-orm-rocket-okapi-example" +version = "0.1.0" +authors = ["Sam Samai ", "Erick Pacheco "] +edition = "2021" +publish = false + +[dependencies] +async-stream = { version = "^0.3" } +async-trait = { version = "0.1" } +rocket-example-core = { path = "../core" } +futures = { version = "^0.3" } +futures-util = { version = "^0.3" } +rocket = { version = "0.5.0-rc.1", features = [ + "json", +] } +rocket_dyn_templates = { version = "0.1.0-rc.1", features = [ + "tera", +] } +serde_json = { version = "^1" } +entity = { path = "../entity" } +migration = { path = "../migration" } +tokio = "1.20.0" +serde = "1.0" +dto = { path = "../dto" } + +[dependencies.sea-orm-rocket] +path = "../../../sea-orm-rocket/lib" # remove this line in your own project and use the git line +features = ["rocket_okapi"] #enables rocket_okapi so to have open api features enabled +# git = "https://github.com/SeaQL/sea-orm" + +[dependencies.rocket_okapi] +version = "0.8.0-rc.2" +features = ["swagger", "rapidoc","rocket_db_pools"] + +[dependencies.rocket_cors] +git = "https://github.com/lawliet89/rocket_cors.git" +rev = "54fae070" +default-features = false \ No newline at end of file diff --git a/examples/rocket_okapi_example/api/src/error.rs b/examples/rocket_okapi_example/api/src/error.rs new file mode 100644 index 000000000..88a27472a --- /dev/null +++ b/examples/rocket_okapi_example/api/src/error.rs @@ -0,0 +1,117 @@ +use rocket::{ + http::{ContentType, Status}, + request::Request, + response::{self, Responder, Response}, +}; +use rocket_okapi::okapi::openapi3::Responses; +use rocket_okapi::okapi::schemars::{self, Map}; +use rocket_okapi::{gen::OpenApiGenerator, response::OpenApiResponderInner, OpenApiError}; + +/// Error messages returned to user +#[derive(Debug, serde::Serialize, schemars::JsonSchema)] +pub struct Error { + /// The title of the error message + pub err: String, + /// The description of the error + pub msg: Option, + // HTTP Status Code returned + #[serde(skip)] + pub http_status_code: u16, +} + +impl OpenApiResponderInner for Error { + fn responses(_generator: &mut OpenApiGenerator) -> Result { + use rocket_okapi::okapi::openapi3::{RefOr, Response as OpenApiReponse}; + + let mut responses = Map::new(); + responses.insert( + "400".to_string(), + RefOr::Object(OpenApiReponse { + description: "\ + # [400 Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400)\n\ + The request given is wrongly formatted or data asked could not be fulfilled. \ + " + .to_string(), + ..Default::default() + }), + ); + responses.insert( + "404".to_string(), + RefOr::Object(OpenApiReponse { + description: "\ + # [404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404)\n\ + This response is given when you request a page that does not exists.\ + " + .to_string(), + ..Default::default() + }), + ); + responses.insert( + "422".to_string(), + RefOr::Object(OpenApiReponse { + description: "\ + # [422 Unprocessable Entity](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422)\n\ + This response is given when you request body is not correctly formatted. \ + ".to_string(), + ..Default::default() + }), + ); + responses.insert( + "500".to_string(), + RefOr::Object(OpenApiReponse { + description: "\ + # [500 Internal Server Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500)\n\ + This response is given when something wend wrong on the server. \ + ".to_string(), + ..Default::default() + }), + ); + Ok(Responses { + responses, + ..Default::default() + }) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + formatter, + "Error `{}`: {}", + self.err, + self.msg.as_deref().unwrap_or("") + ) + } +} + +impl std::error::Error for Error {} + +impl<'r> Responder<'r, 'static> for Error { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + // Convert object to json + let body = serde_json::to_string(&self).unwrap(); + Response::build() + .sized_body(body.len(), std::io::Cursor::new(body)) + .header(ContentType::JSON) + .status(Status::new(self.http_status_code)) + .ok() + } +} + +impl From> for Error { + fn from(err: rocket::serde::json::Error) -> Self { + use rocket::serde::json::Error::*; + match err { + Io(io_error) => Error { + err: "IO Error".to_owned(), + msg: Some(io_error.to_string()), + http_status_code: 422, + }, + Parse(_raw_data, parse_error) => Error { + err: "Parse Error".to_owned(), + msg: Some(parse_error.to_string()), + http_status_code: 422, + }, + } + } +} diff --git a/examples/rocket_okapi_example/api/src/lib.rs b/examples/rocket_okapi_example/api/src/lib.rs new file mode 100644 index 000000000..03cf66b2e --- /dev/null +++ b/examples/rocket_okapi_example/api/src/lib.rs @@ -0,0 +1,139 @@ +#[macro_use] +extern crate rocket; + +use rocket::fairing::{self, AdHoc}; +use rocket::{Build, Rocket}; + +use migration::MigratorTrait; +use sea_orm_rocket::Database; + +use rocket_okapi::mount_endpoints_and_merged_docs; +use rocket_okapi::okapi::openapi3::OpenApi; +use rocket_okapi::rapidoc::{make_rapidoc, GeneralConfig, HideShowConfig, RapiDocConfig}; +use rocket_okapi::settings::UrlObject; +use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig}; + +use rocket::http::Method; +use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; + +mod pool; +use pool::Db; +mod error; +mod okapi_example; + +pub use entity::post; +pub use entity::post::Entity as Post; + +async fn run_migrations(rocket: Rocket) -> fairing::Result { + let conn = &Db::fetch(&rocket).unwrap().conn; + let _ = migration::Migrator::up(conn, None).await; + Ok(rocket) +} + +#[tokio::main] +async fn start() -> Result<(), rocket::Error> { + let mut building_rocket = rocket::build() + .attach(Db::init()) + .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) + .mount( + "/swagger-ui/", + make_swagger_ui(&SwaggerUIConfig { + url: "../v1/openapi.json".to_owned(), + ..Default::default() + }), + ) + .mount( + "/rapidoc/", + make_rapidoc(&RapiDocConfig { + title: Some("Rocket/SeaOrm - RapiDoc documentation | RapiDoc".to_owned()), + general: GeneralConfig { + spec_urls: vec![UrlObject::new("General", "../v1/openapi.json")], + ..Default::default() + }, + hide_show: HideShowConfig { + allow_spec_url_load: false, + allow_spec_file_load: false, + ..Default::default() + }, + ..Default::default() + }), + ) + .attach(cors()); + + let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); + let custom_route_spec = (vec![], custom_openapi_spec()); + mount_endpoints_and_merged_docs! { + building_rocket, "/v1".to_owned(), openapi_settings, + "/additional" => custom_route_spec, + "/okapi-example" => okapi_example::get_routes_and_docs(&openapi_settings), + }; + + building_rocket.launch().await.map(|_| ()) +} + +fn cors() -> Cors { + let allowed_origins = + AllowedOrigins::some_exact(&["http://localhost:8000", "http://127.0.0.1:8000"]); + + let cors = rocket_cors::CorsOptions { + allowed_origins, + allowed_methods: vec![Method::Get, Method::Post, Method::Delete] + .into_iter() + .map(From::from) + .collect(), + allowed_headers: AllowedHeaders::all(), + allow_credentials: true, + ..Default::default() + } + .to_cors() + .unwrap(); + cors +} + +fn custom_openapi_spec() -> OpenApi { + use rocket_okapi::okapi::openapi3::*; + OpenApi { + openapi: OpenApi::default_version(), + info: Info { + title: "SeaOrm-Rocket-Okapi Example".to_owned(), + description: Some("API Docs for Rocket/SeaOrm example".to_owned()), + terms_of_service: Some("https://github.com/SeaQL/sea-orm#license".to_owned()), + contact: Some(Contact { + name: Some("SeaOrm".to_owned()), + url: Some("https://github.com/SeaQL/sea-orm".to_owned()), + email: None, + ..Default::default() + }), + license: Some(License { + name: "MIT".to_owned(), + url: Some("https://github.com/SeaQL/sea-orm/blob/master/LICENSE-MIT".to_owned()), + ..Default::default() + }), + version: env!("CARGO_PKG_VERSION").to_owned(), + ..Default::default() + }, + servers: vec![ + Server { + url: "http://127.0.0.1:8000/v1".to_owned(), + description: Some("Localhost".to_owned()), + ..Default::default() + }, + Server { + url: "https://production-server.com/".to_owned(), + description: Some("Remote development server".to_owned()), + ..Default::default() + }, + ], + ..Default::default() + } +} + +pub fn main() { + let result = start(); + + println!("Rocket: deorbit."); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/examples/rocket_okapi_example/api/src/okapi_example.rs b/examples/rocket_okapi_example/api/src/okapi_example.rs new file mode 100644 index 000000000..69d15d181 --- /dev/null +++ b/examples/rocket_okapi_example/api/src/okapi_example.rs @@ -0,0 +1,166 @@ +use dto::dto; +use rocket::serde::json::Json; +use rocket_example_core::{Mutation, Query}; + +use sea_orm_rocket::Connection; + +use rocket_okapi::okapi::openapi3::OpenApi; + +use crate::error; +use crate::pool; +use pool::Db; + +pub use entity::post; +pub use entity::post::Entity as Post; + +use rocket_okapi::settings::OpenApiSettings; + +use rocket_okapi::{openapi, openapi_get_routes_spec}; + +const DEFAULT_POSTS_PER_PAGE: u64 = 5; + +pub fn get_routes_and_docs(settings: &OpenApiSettings) -> (Vec, OpenApi) { + openapi_get_routes_spec![settings: create, update, list, get_by_id, delete, destroy] +} + +pub type R = std::result::Result, error::Error>; +pub type DataResult<'a, T> = + std::result::Result, rocket::serde::json::Error<'a>>; + +/// # Add a new post +#[openapi(tag = "POST")] +#[post("/", data = "")] +async fn create( + conn: Connection<'_, Db>, + post_data: DataResult<'_, post::Model>, +) -> R> { + let db = conn.into_inner(); + let form = post_data?.into_inner(); + let cmd = Mutation::create_post(db, form); + match cmd.await { + Ok(_) => Ok(Json(Some("Post successfully added.".to_string()))), + Err(e) => { + let m = error::Error { + err: "Could not insert post".to_string(), + msg: Some(e.to_string()), + http_status_code: 400, + }; + Err(m) + } + } +} + +/// # Update a post +#[openapi(tag = "POST")] +#[post("/", data = "")] +async fn update( + conn: Connection<'_, Db>, + id: i32, + post_data: DataResult<'_, post::Model>, +) -> R> { + let db = conn.into_inner(); + + let form = post_data?.into_inner(); + + let cmd = Mutation::update_post_by_id(db, id, form); + match cmd.await { + Ok(_) => Ok(Json(Some("Post successfully updated.".to_string()))), + Err(e) => { + let m = error::Error { + err: "Could not update post".to_string(), + msg: Some(e.to_string()), + http_status_code: 400, + }; + Err(m) + } + } +} + +/// # Get post list +#[openapi(tag = "POST")] +#[get("/?&")] +async fn list( + conn: Connection<'_, Db>, + page: Option, + posts_per_page: Option, +) -> R { + let db = conn.into_inner(); + + // Set page number and items per page + let page = page.unwrap_or(1); + let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + if page == 0 { + let m = error::Error { + err: "error getting posts".to_string(), + msg: Some("'page' param cannot be zero".to_string()), + http_status_code: 400, + }; + return Err(m); + } + + let (posts, num_pages) = Query::find_posts_in_page(db, page, posts_per_page) + .await + .expect("Cannot find posts in page"); + + Ok(Json(dto::PostsDto { + page, + posts_per_page, + num_pages, + posts, + })) +} + +/// # get post by Id +#[openapi(tag = "POST")] +#[get("/")] +async fn get_by_id(conn: Connection<'_, Db>, id: i32) -> R> { + let db = conn.into_inner(); + + let post: Option = Query::find_post_by_id(db, id) + .await + .expect("could not find post"); + Ok(Json(post)) +} + +/// # delete post by Id +#[openapi(tag = "POST")] +#[delete("/")] +async fn delete(conn: Connection<'_, Db>, id: i32) -> R> { + let db = conn.into_inner(); + + let cmd = Mutation::delete_post(db, id); + match cmd.await { + Ok(_) => Ok(Json(Some("Post successfully deleted.".to_string()))), + Err(e) => { + let m = error::Error { + err: "Error deleting post".to_string(), + msg: Some(e.to_string()), + http_status_code: 400, + }; + Err(m) + } + } +} + +/// # delete all posts +#[openapi(tag = "POST")] +#[delete("/")] +async fn destroy(conn: Connection<'_, Db>) -> R> { + let db = conn.into_inner(); + + let cmd = Mutation::delete_all_posts(db); + + match cmd.await { + Ok(_) => Ok(Json(Some( + "All Posts were successfully deleted.".to_string(), + ))), + Err(e) => { + let m = error::Error { + err: "Error deleting all posts at once".to_string(), + msg: Some(e.to_string()), + http_status_code: 400, + }; + Err(m) + } + } +} diff --git a/examples/rocket_okapi_example/api/src/pool.rs b/examples/rocket_okapi_example/api/src/pool.rs new file mode 100644 index 000000000..b1c056779 --- /dev/null +++ b/examples/rocket_okapi_example/api/src/pool.rs @@ -0,0 +1,41 @@ +use rocket_example_core::sea_orm; + +use async_trait::async_trait; +use sea_orm::ConnectOptions; +use sea_orm_rocket::{rocket::figment::Figment, Config, Database}; +use std::time::Duration; + +#[derive(Database, Debug)] +#[database("sea_orm")] +pub struct Db(SeaOrmPool); + +#[derive(Debug, Clone)] +pub struct SeaOrmPool { + pub conn: sea_orm::DatabaseConnection, +} + +#[async_trait] +impl sea_orm_rocket::Pool for SeaOrmPool { + type Error = sea_orm::DbErr; + + type Connection = sea_orm::DatabaseConnection; + + async fn init(figment: &Figment) -> Result { + let config = figment.extract::().unwrap(); + let mut options: ConnectOptions = config.url.into(); + options + .max_connections(config.max_connections as u32) + .min_connections(config.min_connections.unwrap_or_default()) + .connect_timeout(Duration::from_secs(config.connect_timeout)); + if let Some(idle_timeout) = config.idle_timeout { + options.idle_timeout(Duration::from_secs(idle_timeout)); + } + let conn = sea_orm::Database::connect(options).await?; + + Ok(SeaOrmPool { conn }) + } + + fn borrow(&self) -> &Self::Connection { + &self.conn + } +} diff --git a/examples/rocket_okapi_example/core/Cargo.toml b/examples/rocket_okapi_example/core/Cargo.toml new file mode 100644 index 000000000..a57a55608 --- /dev/null +++ b/examples/rocket_okapi_example/core/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rocket-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "runtime-tokio-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", +] + +[dev-dependencies] +tokio = "1.20.0" + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/rocket_okapi_example/core/src/lib.rs b/examples/rocket_okapi_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/rocket_okapi_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/rocket_okapi_example/core/src/mutation.rs b/examples/rocket_okapi_example/core/src/mutation.rs new file mode 100644 index 000000000..dd6891d4a --- /dev/null +++ b/examples/rocket_okapi_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/rocket_okapi_example/core/src/query.rs b/examples/rocket_okapi_example/core/src/query.rs new file mode 100644 index 000000000..e8d2668f5 --- /dev/null +++ b/examples/rocket_okapi_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/rocket_okapi_example/core/tests/mock.rs b/examples/rocket_okapi_example/core/tests/mock.rs new file mode 100644 index 000000000..84b187e5c --- /dev/null +++ b/examples/rocket_okapi_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use entity::post; +use prepare::prepare_mock_db; +use rocket_example_core::{Mutation, Query}; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/rocket_okapi_example/core/tests/prepare.rs b/examples/rocket_okapi_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/rocket_okapi_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/rocket_okapi_example/dto/Cargo.toml b/examples/rocket_okapi_example/dto/Cargo.toml new file mode 100644 index 000000000..a0f208dba --- /dev/null +++ b/examples/rocket_okapi_example/dto/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dto" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "dto" +path = "src/lib.rs" + +[dependencies] +rocket = { version = "0.5.0-rc.1", features = [ + "json", +] } + +[dependencies.entity] +path = "../entity" + +[dependencies.rocket_okapi] +version = "0.8.0-rc.2" \ No newline at end of file diff --git a/examples/rocket_okapi_example/dto/src/dto.rs b/examples/rocket_okapi_example/dto/src/dto.rs new file mode 100644 index 000000000..976b2cf0c --- /dev/null +++ b/examples/rocket_okapi_example/dto/src/dto.rs @@ -0,0 +1,12 @@ +use entity::*; +use rocket::serde::{Deserialize, Serialize}; +use rocket_okapi::okapi::schemars::{self, JsonSchema}; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(crate = "rocket::serde")] +pub struct PostsDto { + pub page: u64, + pub posts_per_page: u64, + pub num_pages: u64, + pub posts: Vec, +} diff --git a/examples/rocket_okapi_example/dto/src/lib.rs b/examples/rocket_okapi_example/dto/src/lib.rs new file mode 100644 index 000000000..a07dce5c0 --- /dev/null +++ b/examples/rocket_okapi_example/dto/src/lib.rs @@ -0,0 +1 @@ +pub mod dto; diff --git a/examples/rocket_okapi_example/entity/Cargo.toml b/examples/rocket_okapi_example/entity/Cargo.toml new file mode 100644 index 000000000..c1cf045df --- /dev/null +++ b/examples/rocket_okapi_example/entity/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +rocket = { version = "0.5.0-rc.1", features = [ + "json", +] } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version + +[dependencies.rocket_okapi] +version = "0.8.0-rc.2" diff --git a/examples/rocket_okapi_example/entity/src/lib.rs b/examples/rocket_okapi_example/entity/src/lib.rs new file mode 100644 index 000000000..06480a10b --- /dev/null +++ b/examples/rocket_okapi_example/entity/src/lib.rs @@ -0,0 +1,4 @@ +#[macro_use] +extern crate rocket; + +pub mod post; diff --git a/examples/rocket_okapi_example/entity/src/post.rs b/examples/rocket_okapi_example/entity/src/post.rs new file mode 100644 index 000000000..a5797f484 --- /dev/null +++ b/examples/rocket_okapi_example/entity/src/post.rs @@ -0,0 +1,21 @@ +use rocket::serde::{Deserialize, Serialize}; +use rocket_okapi::okapi::schemars::{self, JsonSchema}; +use sea_orm::entity::prelude::*; + +#[derive( + Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize, FromForm, JsonSchema, +)] +#[serde(crate = "rocket::serde")] +#[sea_orm(table_name = "posts")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub title: String, + #[sea_orm(column_type = "Text")] + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/rocket_okapi_example/migration/Cargo.toml b/examples/rocket_okapi_example/migration/Cargo.toml new file mode 100644 index 000000000..b8251d207 --- /dev/null +++ b/examples/rocket_okapi_example/migration/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +rocket = { version = "0.5.0-rc.1" } +async-std = { version = "^1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "^0.10.0" # sea-orm-migration version +features = [ + # Enable following runtime and db backend features if you want to run migration via CLI + # "runtime-tokio-native-tls", + # "sqlx-postgres", +] diff --git a/examples/rocket_okapi_example/migration/README.md b/examples/rocket_okapi_example/migration/README.md new file mode 100644 index 000000000..963caaeb6 --- /dev/null +++ b/examples/rocket_okapi_example/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/rocket_okapi_example/migration/src/lib.rs b/examples/rocket_okapi_example/migration/src/lib.rs new file mode 100644 index 000000000..af8d9b2ac --- /dev/null +++ b/examples/rocket_okapi_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_orm_migration::prelude::*; + +mod m20220120_000001_create_post_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_post_table::Migration)] + } +} diff --git a/examples/rocket_okapi_example/migration/src/m20220120_000001_create_post_table.rs b/examples/rocket_okapi_example/migration/src/m20220120_000001_create_post_table.rs new file mode 100644 index 000000000..a2fa0219c --- /dev/null +++ b/examples/rocket_okapi_example/migration/src/m20220120_000001_create_post_table.rs @@ -0,0 +1,42 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Posts::Table) + .if_not_exists() + .col( + ColumnDef::new(Posts::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Posts::Title).string().not_null()) + .col(ColumnDef::new(Posts::Text).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Posts::Table).to_owned()) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum Posts { + Table, + Id, + Title, + Text, +} diff --git a/examples/rocket_okapi_example/migration/src/main.rs b/examples/rocket_okapi_example/migration/src/main.rs new file mode 100644 index 000000000..4626e82f7 --- /dev/null +++ b/examples/rocket_okapi_example/migration/src/main.rs @@ -0,0 +1,17 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + // Setting `DATABASE_URL` environment variable + let key = "DATABASE_URL"; + if std::env::var(key).is_err() { + // Getting the database URL from Rocket.toml if it's not set + let figment = rocket::Config::figment(); + let database_url: String = figment + .extract_inner("databases.sea_orm.url") + .expect("Cannot find Database URL in Rocket.toml"); + std::env::set_var(key, database_url); + } + + cli::run_cli(migration::Migrator).await; +} diff --git a/examples/rocket_okapi_example/rapidoc.png b/examples/rocket_okapi_example/rapidoc.png new file mode 100644 index 000000000..f3f6c55c6 Binary files /dev/null and b/examples/rocket_okapi_example/rapidoc.png differ diff --git a/examples/rocket_okapi_example/src/main.rs b/examples/rocket_okapi_example/src/main.rs new file mode 100644 index 000000000..182a6875b --- /dev/null +++ b/examples/rocket_okapi_example/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + rocket_example_api::main(); +} diff --git a/examples/rocket_okapi_example/swagger.png b/examples/rocket_okapi_example/swagger.png new file mode 100644 index 000000000..b929be4f0 Binary files /dev/null and b/examples/rocket_okapi_example/swagger.png differ diff --git a/sea-orm-rocket/lib/Cargo.toml b/sea-orm-rocket/lib/Cargo.toml index c2be95086..48797a654 100644 --- a/sea-orm-rocket/lib/Cargo.toml +++ b/sea-orm-rocket/lib/Cargo.toml @@ -24,3 +24,8 @@ version = "0.5.0-rc.1" version = "0.5.0-rc.1" default-features = false features = ["json"] + +[dependencies.rocket_okapi] +version = "0.8.0-rc.2" +default-features = false +optional = true diff --git a/sea-orm-rocket/lib/src/database.rs b/sea-orm-rocket/lib/src/database.rs index 8826b5f95..61ddf16fa 100644 --- a/sea-orm-rocket/lib/src/database.rs +++ b/sea-orm-rocket/lib/src/database.rs @@ -9,6 +9,11 @@ use rocket::{error, info_, Build, Ignite, Phase, Rocket, Sentinel}; use rocket::figment::providers::Serialized; use rocket::yansi::Paint; +#[cfg(feature = "rocket_okapi")] +use rocket_okapi::gen::OpenApiGenerator; +#[cfg(feature = "rocket_okapi")] +use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; + use crate::Pool; /// Derivable trait which ties a database [`Pool`] with a configuration name. @@ -205,6 +210,17 @@ impl<'a, D: Database> Connection<'a, D> { } } +#[cfg(feature = "rocket_okapi")] +impl<'r, D: Database> OpenApiFromRequest<'r> for Connection<'r, D> { + fn from_request_input( + _gen: &mut OpenApiGenerator, + _name: String, + _required: bool, + ) -> rocket_okapi::Result { + Ok(RequestHeaderInput::None) + } +} + #[rocket::async_trait] impl Fairing for Initializer { fn info(&self) -> Info {