diff --git a/sea-orm-pro/docs/02-install-and-config/01-getting-started-loco.md b/sea-orm-pro/docs/02-install-and-config/01-getting-started-loco.md index 7b79d81f44..76d771dd48 100644 --- a/sea-orm-pro/docs/02-install-and-config/01-getting-started-loco.md +++ b/sea-orm-pro/docs/02-install-and-config/01-getting-started-loco.md @@ -15,7 +15,7 @@ cd sea-orm/examples/loco_starter # Create a directory for the static assets mkdir assets -# Use the `download_frontend.sh` to download SeaORM Pro FREE admin panel to `assets/admin` directory +# Use the `download_frontend.sh` to download SeaORM Pro admin panel to `assets/admin` directory curl "https://raw.githubusercontent.com/SeaQL/sea-orm-pro/refs/heads/main/build_tools/download_frontend.sh" -sSf | sh ``` @@ -47,7 +47,7 @@ pub async fn config(State(_ctx): State) -> Result { format::json(serde_json::json!({ "site": { "theme": { - "title": "SeaORM Pro FREE", + "title": "SeaORM Pro", "logo": "/admin/favicon.ico", "login_banner": "/admin/logo.png", } @@ -418,12 +418,17 @@ listening on localhost:3000 Visit [http://localhost:3000/admin](http://localhost:3000/admin) and login with your registered user. -![](../../static/img/getting-started-01-login.png) +``` +Username: demo@sea-ql.org +Password: demo@sea-ql.org +``` + +![](../../static/img/getting-started-loco-01-login.png) -![](../../static/img/getting-started-02-raw-tables.png) +![](../../static/img/getting-started-loco-02-raw-tables.png) -![](../../static/img/getting-started-03-raw-tables-notes.png) +![](../../static/img/getting-started-loco-03-raw-tables-notes.png) -![](../../static/img/getting-started-04-raw-tables-notes-details.png) +![](../../static/img/getting-started-loco-04-raw-tables-notes-details.png) -![](../../static/img/getting-started-05-composite-tables.png) +![](../../static/img/getting-started-loco-05-composite-tables.png) diff --git a/sea-orm-pro/docs/02-install-and-config/02-getting-started-axum.md b/sea-orm-pro/docs/02-install-and-config/02-getting-started-axum.md index cc5bda993d..cfbf9f17f5 100644 --- a/sea-orm-pro/docs/02-install-and-config/02-getting-started-axum.md +++ b/sea-orm-pro/docs/02-install-and-config/02-getting-started-axum.md @@ -1,2 +1,299 @@ # Getting Started with Axum +We use the [`axum_example`](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) example as the base, it contains basic REST API to handle basic user management such as user login and user info of current session. + +## 1. Setup Admin Endpoint + +We will extends the SeaORM entities in the example to define a GraphQL schema, handle GraphQL request and serve SeaORM Pro admin panel. + +### 1.1 Download frontend assets + +```sh +# Go to the api crate of `axum_example` example +cd sea-orm/examples/axum_example/api + +# Create a directory for the static assets +mkdir assets + +# Use the `download_frontend.sh` to download SeaORM Pro admin panel to `assets/admin` directory +curl "https://raw.githubusercontent.com/SeaQL/sea-orm-pro/refs/heads/main/build_tools/download_frontend.sh" -sSf | sh +``` + +### 1.2 Serve frontend via `static` middleware + +Open the `lib.rs` of api crate, add `static` middlewares. The admin panel frontend is located in `/assets/admin` and we want to serve it under `http://localhost:8000/admin`, so we set the path as `/assets/admin`. Also, the admin panel frontend is a single page application, so we set a fallback route to the index file, `/assets/admin/index.html`. + +```diff title=examples/axum_example/api/src/lib.rs +#[tokio::main] +async fn start() -> anyhow::Result<()> { + let app = Router::new() + .route("/api/auth/login", post(user_login)) + .route("/api/user/current", get(current_user)) ++ .nest_service( ++ "/admin", ++ get_service( ++ ServeDir::new(concat!( ++ env!("CARGO_MANIFEST_DIR"), ++ "/assets/admin" ++ )) ++ .fallback(ServeFile::new(concat!( ++ env!("CARGO_MANIFEST_DIR"), ++ "/assets/admin/index.html" ++ ))) ++ ) ++ ); + + let listener = tokio::net::TcpListener::bind(&server_url).await.unwrap(); + axum::serve(listener, app).await?; + + Ok(()) +} +``` + +### 1.3 Setup admin API endpoint + +The admin panel frontend is customizable and it read the configuration from the `api/admin/config` endpoint. + +```rust title=examples/axum_example/api/src/lib.rs +async fn admin_panel_config(_state: State) -> Result, (StatusCode, &'static str)> { + Ok(Json(serde_json::json!({ + "site": { + "theme": { + "title": "SeaORM Pro FREE", + "logo": "/admin/favicon.ico", + "login_banner": "/admin/logo.png", + } + }, + "raw_tables": {}, + "composite_tables": {}, + }))) +} +``` + +Use the admin controller and register the `/api/admin` route. + +```diff title=examples/axum_example/api/src/lib.rs +#[tokio::main] +async fn start() -> anyhow::Result<()> { + let app = Router::new() ++ .route("/api/admin/config", get(admin_panel_config)) + .route("/api/auth/login", post(user_login)) + .route("/api/user/current", get(current_user)) + .nest_service( + "/admin", + get_service( + ServeDir::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/admin" + )) + .fallback(ServeFile::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/admin/index.html" + ))) + ) + ); + + let listener = tokio::net::TcpListener::bind(&server_url).await.unwrap(); + axum::serve(listener, app).await?; + + Ok(()) +} +``` + +## 2. Setup GraphQL Endpoint + +### 2.1 Define GraphQL schema + +Add the dependencies for defining GraphQL schema: `async-graphql`, `seaography` and `lazy_static`. + +```diff title=examples/axum_example/api/Cargo.toml +[dependencies] ++ async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } ++ seaography = { version = "1.1.0", features = ["with-decimal", "with-chrono", "with-uuid", "field-snake-case"] } ++ lazy_static = { version = "1.4" } +``` + +We need to define an `RelatedEntity` enum for each of the SeaORM entity to help `seaography` figure out the parent-child relation between entities. + +```diff title=examples/axum_example/entity/src/post.rs +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "posts")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + 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 {} + ++ #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] ++ pub enum RelatedEntity {} +``` + +Use `seaography::register_entity_modules!` macros to define `register_entity_modules` boilerplate function. + +```diff title=examples/axum_example/api/src/graphql/query_root.rs ++ seaography::register_entity_modules!([posts]); +``` + +```rust title=examples/axum_example/api/src/graphql/query_root.rs +use async_graphql::dynamic::*; +use axum_example_service::sea_orm; +use axum_example_service::sea_orm::DatabaseConnection; +use seaography::{Builder, BuilderContext}; + +lazy_static::lazy_static! { + static ref CONTEXT: BuilderContext = BuilderContext::default(); +} + +pub fn schema( + database: DatabaseConnection, + depth: Option, + complexity: Option, +) -> Result { + // Construct GraphQL schema + let builder = Builder::new(&CONTEXT, database.clone()); + let builder = register_entity_modules(builder); + let schema = builder.schema_builder(); + // Maximum depth of the constructed query + let schema = if let Some(depth) = depth { + schema.limit_depth(depth) + } else { + schema + }; + // Maximum complexity of the constructed query + let schema = if let Some(complexity) = complexity { + schema.limit_complexity(complexity) + } else { + schema + }; + // GraphQL schema with database connection + schema.data(database).finish() +} +``` + +Use the GraphQL schema module. + +```rust title=examples/axum_example/api/src/graphql/mod.rs +pub mod query_root; +``` + +```diff title=examples/axum_example/api/src/lib.rs ++ mod graphql; +``` + +### 2.2 Setup GraphQL playground and query root + +Add dependencies for serving GraphQL playground and handling GraphQL request. + +```diff title= +[dependencies] +async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } ++ async-graphql-axum = { version = "7.0" } +seaography = { version = "1.1.0", features = ["with-decimal", "with-chrono", "with-uuid", "field-snake-case"] } +lazy_static = { version = "1.4" } +``` + +The GraphQL controller. + +```rust title=examples/axum_example/api/Cargo.toml +async fn graphql_playground() -> impl IntoResponse { + // Setup GraphQL playground web and specify the endpoint for GraphQL resolver + let res = playground_source(GraphQLPlaygroundConfig::new("/api/graphql")); + + Html(res) +} + +async fn graphql_handler( + state: State, + headers: HeaderMap, + req: GraphQLRequest, +) -> Result { + check_user_auth(&headers)?; + // Maximum depth of the constructed query + const DEPTH: Option = None; + // Maximum complexity of the constructed query + const COMPLEXITY: Option = None; + // GraphQL schema + let schema = graphql::query_root::schema(state.conn.clone(), DEPTH, COMPLEXITY).unwrap(); + // GraphQL handler + let res = schema.execute(req.into_inner()).await.into(); + Ok(res) +} +``` + +Use the GraphQL controller and register the `/api/graphql` route. + +```diff title=examples/axum_example/api/src/lib.rs +#[tokio::main] +async fn start() -> anyhow::Result<()> { + let app = Router::new() + .route("/api/admin/config", get(admin_panel_config)) + .route("/api/auth/login", post(user_login)) + .route("/api/user/current", get(current_user)) ++ .route("/api/graphql", get(graphql_playground)) ++ .route("/api/graphql", post(graphql_handler)) + .nest_service( + "/admin", + get_service( + ServeDir::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/admin" + )) + .fallback(ServeFile::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/admin/index.html" + ))) + ) + ); + + let listener = tokio::net::TcpListener::bind(&server_url).await.unwrap(); + axum::serve(listener, app).await?; + + Ok(()) +} +``` + +## 3. Launch! + +```sh +$ cargo run + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.44s + Running `target/debug/sea-orm-axum-example` +2025-01-08T08:08:00.048868Z DEBUG sea_orm::database::transaction: CREATE TABLE IF NOT EXISTS "seaql_migrations" ( "version" varchar NOT NULL PRIMARY KEY, "applied_at" bigint NOT NULL ) +2025-01-08T08:08:00.050610Z INFO sqlx::postgres::notice: relation "seaql_migrations" already exists, skipping +2025-01-08T08:08:00.052499Z INFO sqlx::query: summary="CREATE TABLE IF NOT …" db.statement="\n\nCREATE TABLE IF NOT EXISTS \"seaql_migrations\" (\n \"version\" varchar NOT NULL PRIMARY KEY,\n \"applied_at\" bigint NOT NULL\n)\n" rows_affected=0 rows_returned=0 elapsed=1.731155ms elapsed_secs=0.001731155 +2025-01-08T08:08:00.052619Z INFO sea_orm_migration::migrator: Applying all pending migrations +2025-01-08T08:08:00.062417Z INFO sea_orm_migration::migrator: No pending migrations +2025-01-08T08:08:00.062963Z INFO sqlx::query: summary="COMMIT" db.statement="" rows_affected=0 rows_returned=0 elapsed=495.319µs elapsed_secs=0.000495319 +2025-01-08T08:08:00.063398Z DEBUG globset: glob converted to regex: Glob { glob: "**/*", re: "(?-u)^(?:/?|.*/)[^/]*$", opts: GlobOptions { case_insensitive: false, literal_separator: true, backslash_escape: true, empty_alternates: false }, tokens: Tokens([RecursivePrefix, ZeroOrMore]) } +2025-01-08T08:08:00.063439Z DEBUG globset: built glob set; 0 literals, 0 basenames, 0 extensions, 0 prefixes, 0 suffixes, 0 required extensions, 1 regexes + +``` + +Visit [http://localhost:8000/admin](http://localhost:8000/admin) and login with your registered user. + +``` +Username: demo@sea-ql.org +Password: demo@sea-ql.org +``` + +![](../../static/img/getting-started-axum-01-login.png) + +![](../../static/img/getting-started-axum-02-raw-tables.png) + +![](../../static/img/getting-started-axum-03-raw-tables-posts.png) + +![](../../static/img/getting-started-axum-04-raw-tables-posts-details.png) + +![](../../static/img/getting-started-axum-05-composite-tables.png) diff --git a/sea-orm-pro/docs/03-site-config/01-theme.md b/sea-orm-pro/docs/03-site-config/01-theme.md index eb6b8e1d0b..1a94e78968 100644 --- a/sea-orm-pro/docs/03-site-config/01-theme.md +++ b/sea-orm-pro/docs/03-site-config/01-theme.md @@ -6,7 +6,7 @@ [site.theme] # Title of admin panel -title = "SeaORM Pro FREE" +title = "SeaORM Pro" # File path of admin panel logo logo = "/admin/favicon.ico" diff --git a/sea-orm-pro/static/img/getting-started-axum-01-login.png b/sea-orm-pro/static/img/getting-started-axum-01-login.png new file mode 100644 index 0000000000..80eab0084a Binary files /dev/null and b/sea-orm-pro/static/img/getting-started-axum-01-login.png differ diff --git a/sea-orm-pro/static/img/getting-started-axum-02-raw-tables.png b/sea-orm-pro/static/img/getting-started-axum-02-raw-tables.png new file mode 100644 index 0000000000..45945b90c2 Binary files /dev/null and b/sea-orm-pro/static/img/getting-started-axum-02-raw-tables.png differ diff --git a/sea-orm-pro/static/img/getting-started-axum-03-raw-tables-posts.png b/sea-orm-pro/static/img/getting-started-axum-03-raw-tables-posts.png new file mode 100644 index 0000000000..19d702d995 Binary files /dev/null and b/sea-orm-pro/static/img/getting-started-axum-03-raw-tables-posts.png differ diff --git a/sea-orm-pro/static/img/getting-started-axum-04-raw-tables-posts-details.png b/sea-orm-pro/static/img/getting-started-axum-04-raw-tables-posts-details.png new file mode 100644 index 0000000000..d759127e38 Binary files /dev/null and b/sea-orm-pro/static/img/getting-started-axum-04-raw-tables-posts-details.png differ diff --git a/sea-orm-pro/static/img/getting-started-axum-05-composite-tables.png b/sea-orm-pro/static/img/getting-started-axum-05-composite-tables.png new file mode 100644 index 0000000000..ccf56131c2 Binary files /dev/null and b/sea-orm-pro/static/img/getting-started-axum-05-composite-tables.png differ diff --git a/sea-orm-pro/static/img/getting-started-01-login.png b/sea-orm-pro/static/img/getting-started-loco-01-login.png similarity index 100% rename from sea-orm-pro/static/img/getting-started-01-login.png rename to sea-orm-pro/static/img/getting-started-loco-01-login.png diff --git a/sea-orm-pro/static/img/getting-started-02-raw-tables.png b/sea-orm-pro/static/img/getting-started-loco-02-raw-tables.png similarity index 100% rename from sea-orm-pro/static/img/getting-started-02-raw-tables.png rename to sea-orm-pro/static/img/getting-started-loco-02-raw-tables.png diff --git a/sea-orm-pro/static/img/getting-started-03-raw-tables-notes.png b/sea-orm-pro/static/img/getting-started-loco-03-raw-tables-notes.png similarity index 100% rename from sea-orm-pro/static/img/getting-started-03-raw-tables-notes.png rename to sea-orm-pro/static/img/getting-started-loco-03-raw-tables-notes.png diff --git a/sea-orm-pro/static/img/getting-started-04-raw-tables-notes-details.png b/sea-orm-pro/static/img/getting-started-loco-04-raw-tables-notes-details.png similarity index 100% rename from sea-orm-pro/static/img/getting-started-04-raw-tables-notes-details.png rename to sea-orm-pro/static/img/getting-started-loco-04-raw-tables-notes-details.png diff --git a/sea-orm-pro/static/img/getting-started-05-composite-tables.png b/sea-orm-pro/static/img/getting-started-loco-05-composite-tables.png similarity index 100% rename from sea-orm-pro/static/img/getting-started-05-composite-tables.png rename to sea-orm-pro/static/img/getting-started-loco-05-composite-tables.png