diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fbeeb88e..c865b097 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -293,7 +293,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - path: [basic, actix_example, actix4_example, axum_example, rocket_example, poem_example] + path: [basic, actix_example, actix4_example, axum_example, axum-graphql_example, rocket_example, poem_example] steps: - uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index ef5ef548..1e1094c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Enhancements * Codegen add serde derives to enums, if specified https://github.com/SeaQL/sea-orm/pull/463 +* Codegen Unsigned Integer - 2 https://github.com/SeaQL/sea-orm/pull/397 ### Breaking changes diff --git a/README.md b/README.md index 529973d4..8d2a3de3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## Getting Started -[![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=1)](https://GitHub.com/SeaQL/sea-orm/stargazers/) +[![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) If you like what we do, consider starring, commenting, sharing and contributing! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) @@ -29,6 +29,7 @@ Join our Discord server to chat with others in the SeaQL community! + [Rocket Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) + [Actix Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) + [Axum Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) ++ [Axum-GraphQL Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum-graphql_example) + [Poem Example](https://github.com/SeaQL/sea-orm/tree/master/examples/poem_example) ## Features diff --git a/examples/axum-graphql_example/.env b/examples/axum-graphql_example/.env new file mode 100644 index 00000000..f1cc324c --- /dev/null +++ b/examples/axum-graphql_example/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:./db?mode=rwc \ No newline at end of file diff --git a/examples/axum-graphql_example/.gitignore b/examples/axum-graphql_example/.gitignore new file mode 100644 index 00000000..8503cc4c --- /dev/null +++ b/examples/axum-graphql_example/.gitignore @@ -0,0 +1,3 @@ +db +db-shm +db-wal \ No newline at end of file diff --git a/examples/axum-graphql_example/Cargo.toml b/examples/axum-graphql_example/Cargo.toml new file mode 100644 index 00000000..6f8dd51c --- /dev/null +++ b/examples/axum-graphql_example/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "axum-graphql" +authors = ["Aaron Leopold "] +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = [".", "entity", "migration"] + +[dependencies] +tokio = { version = "1.0", features = ["full"] } +axum = "0.4.8" +dotenv = "0.15.0" +async-graphql-axum = "3.0.31" +entity = { path = "entity" } +migration = { path = "migration" } diff --git a/examples/axum-graphql_example/README.md b/examples/axum-graphql_example/README.md new file mode 100644 index 00000000..9111c7cc --- /dev/null +++ b/examples/axum-graphql_example/README.md @@ -0,0 +1,13 @@ +![screenshot](Screenshot1.png) + +![screenshot](Screenshot2.png) + +# Axum-GraphQL with SeaORM example app + +1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database + +1. Turn on the appropriate database feature for your chosen db in `entity/Cargo.toml` (the `"sqlx-sqlite",` line) + +1. Execute `cargo run` to start the server + +1. Visit [localhost:3000/api/graphql](http://localhost:3000/api/graphql) in browser diff --git a/examples/axum-graphql_example/Screenshot1.png b/examples/axum-graphql_example/Screenshot1.png new file mode 100644 index 00000000..d2d81dbb Binary files /dev/null and b/examples/axum-graphql_example/Screenshot1.png differ diff --git a/examples/axum-graphql_example/Screenshot2.png b/examples/axum-graphql_example/Screenshot2.png new file mode 100644 index 00000000..171199c5 Binary files /dev/null and b/examples/axum-graphql_example/Screenshot2.png differ diff --git a/examples/axum-graphql_example/entity/Cargo.toml b/examples/axum-graphql_example/entity/Cargo.toml new file mode 100644 index 00000000..eecccb61 --- /dev/null +++ b/examples/axum-graphql_example/entity/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } + +[dependencies.async-graphql] +version = "3.0.12" + +[dependencies.sea-orm] +version = "^0.6.0" +features = [ + "macros", + "runtime-tokio-native-tls", + # "sqlx-postgres", + # "sqlx-mysql", + "sqlx-sqlite" +] +default-features = false \ No newline at end of file diff --git a/examples/axum-graphql_example/entity/src/lib.rs b/examples/axum-graphql_example/entity/src/lib.rs new file mode 100644 index 00000000..e9cf63e5 --- /dev/null +++ b/examples/axum-graphql_example/entity/src/lib.rs @@ -0,0 +1,4 @@ +pub mod note; + +pub use async_graphql; +pub use sea_orm; diff --git a/examples/axum-graphql_example/entity/src/note.rs b/examples/axum-graphql_example/entity/src/note.rs new file mode 100644 index 00000000..d1d30750 --- /dev/null +++ b/examples/axum-graphql_example/entity/src/note.rs @@ -0,0 +1,39 @@ +use async_graphql::*; +use sea_orm::{entity::prelude::*, DeleteMany}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)] +#[sea_orm(table_name = "notes")] +#[graphql(concrete(name = "Note", params()))] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + pub id: i32, + pub title: String, + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + panic!("No RelationDef") + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_id(id: i32) -> Select { + Self::find().filter(Column::Id.eq(id)) + } + + pub fn find_by_title(title: &str) -> Select { + Self::find().filter(Column::Title.eq(title)) + } + + pub fn delete_by_id(id: i32) -> DeleteMany { + Self::delete_many().filter(Column::Id.eq(id)) + } +} diff --git a/examples/axum-graphql_example/migration/Cargo.toml b/examples/axum-graphql_example/migration/Cargo.toml new file mode 100644 index 00000000..196deacc --- /dev/null +++ b/examples/axum-graphql_example/migration/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "0.5.0", default-features = false, features = [ "migration", "debug-print" ] } +dotenv = "0.15.0" +entity = { path = "../entity" } \ No newline at end of file diff --git a/examples/axum-graphql_example/migration/README.md b/examples/axum-graphql_example/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/examples/axum-graphql_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/axum-graphql_example/migration/src/lib.rs b/examples/axum-graphql_example/migration/src/lib.rs new file mode 100644 index 00000000..339d693b --- /dev/null +++ b/examples/axum-graphql_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::*; + +mod m20220101_000001_create_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220101_000001_create_table::Migration)] + } +} diff --git a/examples/axum-graphql_example/migration/src/m20220101_000001_create_table.rs b/examples/axum-graphql_example/migration/src/m20220101_000001_create_table.rs new file mode 100644 index 00000000..4f2147e9 --- /dev/null +++ b/examples/axum-graphql_example/migration/src/m20220101_000001_create_table.rs @@ -0,0 +1,52 @@ +use entity::{ + note, + sea_orm::{DbBackend, EntityTrait, Schema}, +}; +use sea_schema::migration::{ + sea_query::{self, *}, + *, +}; + +pub struct Migration; + +fn get_seaorm_create_stmt(e: E) -> TableCreateStatement { + let schema = Schema::new(DbBackend::Sqlite); + + schema + .create_table_from_entity(e) + .if_not_exists() + .to_owned() +} + +fn get_seaorm_drop_stmt(e: E) -> TableDropStatement { + Table::drop().table(e).if_exists().to_owned() +} + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220101_000001_create_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let stmts = vec![get_seaorm_create_stmt(note::Entity)]; + + for stmt in stmts { + manager.create_table(stmt.to_owned()).await?; + } + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let stmts = vec![get_seaorm_drop_stmt(note::Entity)]; + + for stmt in stmts { + manager.drop_table(stmt.to_owned()).await?; + } + + Ok(()) + } +} diff --git a/examples/axum-graphql_example/migration/src/main.rs b/examples/axum-graphql_example/migration/src/main.rs new file mode 100644 index 00000000..5a5548f8 --- /dev/null +++ b/examples/axum-graphql_example/migration/src/main.rs @@ -0,0 +1,26 @@ +use migration::Migrator; +use sea_schema::migration::*; +use std::path::PathBuf; + +#[cfg(debug_assertions)] +use dotenv::dotenv; + +#[async_std::main] +async fn main() { + #[cfg(debug_assertions)] + dotenv().ok(); + + let fallback = "sqlite:./db?mode=rwc"; + + match std::env::var("DATABASE_URL") { + Ok(val) => { + println!("Using DATABASE_URL: {}", val); + } + Err(_) => { + std::env::set_var("DATABASE_URL", fallback); + println!("Set DATABASE_URL: {}", fallback); + } + }; + + cli::run_cli(Migrator).await; +} diff --git a/examples/axum-graphql_example/src/db.rs b/examples/axum-graphql_example/src/db.rs new file mode 100644 index 00000000..da323597 --- /dev/null +++ b/examples/axum-graphql_example/src/db.rs @@ -0,0 +1,20 @@ +use entity::sea_orm; +use sea_orm::DatabaseConnection; + +pub struct Database { + pub connection: DatabaseConnection, +} + +impl Database { + pub async fn new() -> Self { + let connection = sea_orm::Database::connect(std::env::var("DATABASE_URL").unwrap()) + .await + .expect("Could not connect to database"); + + Database { connection } + } + + pub fn get_connection(&self) -> &DatabaseConnection { + &self.connection + } +} diff --git a/examples/axum-graphql_example/src/graphql/mod.rs b/examples/axum-graphql_example/src/graphql/mod.rs new file mode 100644 index 00000000..c9d58394 --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/mod.rs @@ -0,0 +1,3 @@ +pub mod mutation; +pub mod query; +pub mod schema; diff --git a/examples/axum-graphql_example/src/graphql/mutation/mod.rs b/examples/axum-graphql_example/src/graphql/mutation/mod.rs new file mode 100644 index 00000000..fb3c483a --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/mutation/mod.rs @@ -0,0 +1,10 @@ +use entity::async_graphql; + +pub mod note; + +pub use note::NoteMutation; + +// Add your other ones here to create a unified Mutation object +// e.x. Mutation(NoteMutation, OtherMutation, OtherOtherMutation) +#[derive(async_graphql::MergedObject, Default)] +pub struct Mutation(NoteMutation); diff --git a/examples/axum-graphql_example/src/graphql/mutation/note.rs b/examples/axum-graphql_example/src/graphql/mutation/note.rs new file mode 100644 index 00000000..0ead996f --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/mutation/note.rs @@ -0,0 +1,60 @@ +use async_graphql::{Context, Object, Result}; +use entity::async_graphql::{self, InputObject, SimpleObject}; +use entity::note; +use entity::sea_orm::{ActiveModelTrait, Set}; + +use crate::db::Database; + +// I normally separate the input types into separate files/modules, but this is just +// a quick example. + +#[derive(InputObject)] +pub struct CreateNoteInput { + pub title: String, + pub text: String, +} + +#[derive(SimpleObject)] +pub struct DeleteResult { + pub success: bool, + pub rows_affected: u64, +} + +#[derive(Default)] +pub struct NoteMutation; + +#[Object] +impl NoteMutation { + pub async fn create_note( + &self, + ctx: &Context<'_>, + input: CreateNoteInput, + ) -> Result { + let db = ctx.data::().unwrap(); + + let note = note::ActiveModel { + title: Set(input.title), + text: Set(input.text), + ..Default::default() + }; + + Ok(note.insert(db.get_connection()).await?) + } + + pub async fn delete_note(&self, ctx: &Context<'_>, id: i32) -> Result { + let db = ctx.data::().unwrap(); + + let res = note::Entity::delete_by_id(id) + .exec(db.get_connection()) + .await?; + + if res.rows_affected <= 1 { + Ok(DeleteResult { + success: true, + rows_affected: res.rows_affected, + }) + } else { + unimplemented!() + } + } +} diff --git a/examples/axum-graphql_example/src/graphql/query/mod.rs b/examples/axum-graphql_example/src/graphql/query/mod.rs new file mode 100644 index 00000000..b193647a --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/query/mod.rs @@ -0,0 +1,10 @@ +use entity::async_graphql; + +pub mod note; + +pub use note::NoteQuery; + +// Add your other ones here to create a unified Query object +// e.x. Query(NoteQuery, OtherQuery, OtherOtherQuery) +#[derive(async_graphql::MergedObject, Default)] +pub struct Query(NoteQuery); diff --git a/examples/axum-graphql_example/src/graphql/query/note.rs b/examples/axum-graphql_example/src/graphql/query/note.rs new file mode 100644 index 00000000..1ab1e760 --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/query/note.rs @@ -0,0 +1,28 @@ +use async_graphql::{Context, Object, Result}; +use entity::{async_graphql, note, sea_orm::EntityTrait}; + +use crate::db::Database; + +#[derive(Default)] +pub struct NoteQuery; + +#[Object] +impl NoteQuery { + async fn get_notes(&self, ctx: &Context<'_>) -> Result> { + let db = ctx.data::().unwrap(); + + Ok(note::Entity::find() + .all(db.get_connection()) + .await + .map_err(|e| e.to_string())?) + } + + async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result> { + let db = ctx.data::().unwrap(); + + Ok(note::Entity::find_by_id(id) + .one(db.get_connection()) + .await + .map_err(|e| e.to_string())?) + } +} diff --git a/examples/axum-graphql_example/src/graphql/schema.rs b/examples/axum-graphql_example/src/graphql/schema.rs new file mode 100644 index 00000000..6224c409 --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/schema.rs @@ -0,0 +1,21 @@ +use async_graphql::{EmptySubscription, Schema}; +use entity::async_graphql; +use migration::{Migrator, MigratorTrait}; + +use crate::{ + db::Database, + graphql::{mutation::Mutation, query::Query}, +}; + +pub type AppSchema = Schema; + +/// Builds the GraphQL Schema, attaching the Database to the context +pub async fn build_schema() -> AppSchema { + let db = Database::new().await; + + Migrator::up(db.get_connection(), None).await.unwrap(); + + Schema::build(Query::default(), Mutation::default(), EmptySubscription) + .data(db) + .finish() +} diff --git a/examples/axum-graphql_example/src/main.rs b/examples/axum-graphql_example/src/main.rs new file mode 100644 index 00000000..665f79f2 --- /dev/null +++ b/examples/axum-graphql_example/src/main.rs @@ -0,0 +1,49 @@ +mod db; +mod graphql; + +use entity::async_graphql; + +use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; +use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use axum::{ + extract::Extension, + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use graphql::schema::{build_schema, AppSchema}; + +#[cfg(debug_assertions)] +use dotenv::dotenv; + +async fn graphql_handler(schema: Extension, req: GraphQLRequest) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() +} + +async fn graphql_playground() -> impl IntoResponse { + Html(playground_source(GraphQLPlaygroundConfig::new( + "/api/graphql", + ))) +} + +#[tokio::main] +async fn main() { + #[cfg(debug_assertions)] + dotenv().ok(); + + let schema = build_schema().await; + + let app = Router::new() + .route( + "/api/graphql", + get(graphql_playground).post(graphql_handler), + ) + .layer(Extension(schema)); + + println!("Playground: http://localhost:3000/api/graphql"); + + axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 3e0a96e9..c683dc3a 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -22,7 +22,7 @@ clap = { version = "^2.33.3" } dotenv = { version = "^0.15" } async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] } sea-orm-codegen = { version = "^0.6.0", path = "../sea-orm-codegen" } -sea-schema = { version = "^0.5.0", default-features = false, features = [ +sea-schema = { version = "^0.6.0", default-features = false, features = [ "debug-print", "sqlx-mysql", "sqlx-sqlite", diff --git a/sea-orm-codegen/Cargo.toml b/sea-orm-codegen/Cargo.toml index 2a90badb..c2c13f97 100644 --- a/sea-orm-codegen/Cargo.toml +++ b/sea-orm-codegen/Cargo.toml @@ -15,7 +15,7 @@ name = "sea_orm_codegen" path = "src/lib.rs" [dependencies] -sea-query = { version = "0.21.0" } +sea-query = { version = "0.22.0" } syn = { version = "^1", default-features = false, features = [ "derive", "parsing", diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index 3b374b02..125e671c 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -37,6 +37,10 @@ impl Column { ColumnType::SmallInteger(_) => "i16".to_owned(), ColumnType::Integer(_) => "i32".to_owned(), ColumnType::BigInteger(_) => "i64".to_owned(), + ColumnType::TinyUnsigned(_) => "u8".to_owned(), + ColumnType::SmallUnsigned(_) => "u16".to_owned(), + ColumnType::Unsigned(_) => "u32".to_owned(), + ColumnType::BigUnsigned(_) => "u64".to_owned(), ColumnType::Float(_) => "f32".to_owned(), ColumnType::Double(_) => "f64".to_owned(), ColumnType::Json | ColumnType::JsonBinary => "Json".to_owned(), @@ -90,6 +94,10 @@ impl Column { ColumnType::SmallInteger(_) => quote! { ColumnType::SmallInteger.def() }, ColumnType::Integer(_) => quote! { ColumnType::Integer.def() }, ColumnType::BigInteger(_) => quote! { ColumnType::BigInteger.def() }, + ColumnType::TinyUnsigned(_) => quote! { ColumnType::TinyUnsigned.def() }, + ColumnType::SmallUnsigned(_) => quote! { ColumnType::SmallUnsigned.def() }, + ColumnType::Unsigned(_) => quote! { ColumnType::Unsigned.def() }, + ColumnType::BigUnsigned(_) => quote! { ColumnType::BigUnsigned.def() }, ColumnType::Float(_) => quote! { ColumnType::Float.def() }, ColumnType::Double(_) => quote! { ColumnType::Double.def() }, ColumnType::Decimal(s) => match s { @@ -194,9 +202,13 @@ mod tests { ColumnType::Custom(SeaRc::new(Alias::new("cus_col"))) ), make_col!("CakeId", ColumnType::TinyInteger(None)), + make_col!("CakeId", ColumnType::TinyUnsigned(Some(9))), make_col!("CakeId", ColumnType::SmallInteger(None)), - make_col!("CakeId", ColumnType::Integer(Some(11))), + make_col!("CakeId", ColumnType::SmallUnsigned(Some(10))), + make_col!("CakeId", ColumnType::Integer(None)), + make_col!("CakeId", ColumnType::Unsigned(Some(11))), make_col!("CakeFillingId", ColumnType::BigInteger(None)), + make_col!("CakeFillingId", ColumnType::BigUnsigned(Some(12))), make_col!("cake-filling-id", ColumnType::Float(None)), make_col!("CAKE_FILLING_ID", ColumnType::Double(None)), make_col!("CAKE-FILLING-ID", ColumnType::Binary(None)), @@ -218,6 +230,10 @@ mod tests { "cake_id", "cake_id", "cake_id", + "cake_id", + "cake_id", + "cake_id", + "cake_filling_id", "cake_filling_id", "cake_filling_id", "cake_filling_id", @@ -243,6 +259,10 @@ mod tests { "CakeId", "CakeId", "CakeId", + "CakeId", + "CakeId", + "CakeId", + "CakeFillingId", "CakeFillingId", "CakeFillingId", "CakeFillingId", @@ -266,9 +286,13 @@ mod tests { "String", "String", "i8", + "u8", "i16", + "u16", "i32", + "u32", "i64", + "u64", "f32", "f64", "Vec", @@ -300,9 +324,13 @@ mod tests { "ColumnType::String(Some(255u32)).def()", "ColumnType::Custom(\"cus_col\".to_owned()).def()", "ColumnType::TinyInteger.def()", + "ColumnType::TinyUnsigned.def()", "ColumnType::SmallInteger.def()", + "ColumnType::SmallUnsigned.def()", "ColumnType::Integer.def()", + "ColumnType::Unsigned.def()", "ColumnType::BigInteger.def()", + "ColumnType::BigUnsigned.def()", "ColumnType::Float.def()", "ColumnType::Double.def()", "ColumnType::Binary.def()", diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 21269cb9..a5c4af0d 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -778,28 +778,28 @@ mod tests { }, Column { name: "testing".to_owned(), - col_type: ColumnType::Integer(Some(11)), + col_type: ColumnType::TinyInteger(Some(11)), auto_increment: false, not_null: true, unique: false, }, Column { name: "rust".to_owned(), - col_type: ColumnType::Integer(Some(11)), + col_type: ColumnType::TinyUnsigned(Some(11)), auto_increment: false, not_null: true, unique: false, }, Column { name: "keywords".to_owned(), - col_type: ColumnType::Integer(Some(11)), + col_type: ColumnType::SmallInteger(Some(11)), auto_increment: false, not_null: true, unique: false, }, Column { name: "type".to_owned(), - col_type: ColumnType::Integer(Some(11)), + col_type: ColumnType::SmallUnsigned(Some(11)), auto_increment: false, not_null: true, unique: false, @@ -813,21 +813,21 @@ mod tests { }, Column { name: "crate".to_owned(), - col_type: ColumnType::Integer(Some(11)), + col_type: ColumnType::Unsigned(Some(11)), auto_increment: false, not_null: true, unique: false, }, Column { name: "self".to_owned(), - col_type: ColumnType::Integer(Some(11)), + col_type: ColumnType::BigInteger(Some(11)), auto_increment: false, not_null: true, unique: false, }, Column { name: "self_id1".to_owned(), - col_type: ColumnType::Integer(Some(11)), + col_type: ColumnType::BigUnsigned(Some(11)), auto_increment: false, not_null: true, unique: false, diff --git a/sea-orm-codegen/tests/compact/rust_keyword.rs b/sea-orm-codegen/tests/compact/rust_keyword.rs index 8b76eaa1..c5d46f50 100644 --- a/sea-orm-codegen/tests/compact/rust_keyword.rs +++ b/sea-orm-codegen/tests/compact/rust_keyword.rs @@ -7,14 +7,14 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: i32, - pub testing: i32, - pub rust: i32, - pub keywords: i32, - pub r#type: i32, + pub testing: i8, + pub rust: u8, + pub keywords: i16, + pub r#type: u16, pub r#typeof: i32, - pub crate_: i32, - pub self_: i32, - pub self_id1: i32, + pub crate_: u32, + pub self_: i64, + pub self_id1: u64, pub self_id2: i32, pub fruit_id1: i32, pub fruit_id2: i32, diff --git a/sea-orm-codegen/tests/expanded/rust_keyword.rs b/sea-orm-codegen/tests/expanded/rust_keyword.rs index e0677897..1e96f791 100644 --- a/sea-orm-codegen/tests/expanded/rust_keyword.rs +++ b/sea-orm-codegen/tests/expanded/rust_keyword.rs @@ -14,14 +14,14 @@ impl EntityName for Entity { #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] pub struct Model { pub id: i32, - pub testing: i32, - pub rust: i32, - pub keywords: i32, - pub r#type: i32, + pub testing: i8, + pub rust: u8, + pub keywords: i16, + pub r#type: u16, pub r#typeof: i32, - pub crate_: i32, - pub self_: i32, - pub self_id1: i32, + pub crate_: u32, + pub self_: i64, + pub self_id1: u64, pub self_id2: i32, pub fruit_id1: i32, pub fruit_id2: i32, @@ -73,14 +73,14 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Testing => ColumnType::Integer.def(), - Self::Rust => ColumnType::Integer.def(), - Self::Keywords => ColumnType::Integer.def(), - Self::Type => ColumnType::Integer.def(), + Self::Testing => ColumnType::TinyInteger.def(), + Self::Rust => ColumnType::TinyUnsigned.def(), + Self::Keywords => ColumnType::SmallInteger.def(), + Self::Type => ColumnType::SmallUnsigned.def(), Self::Typeof => ColumnType::Integer.def(), - Self::Crate => ColumnType::Integer.def(), - Self::Self_ => ColumnType::Integer.def(), - Self::SelfId1 => ColumnType::Integer.def(), + Self::Crate => ColumnType::Unsigned.def(), + Self::Self_ => ColumnType::BigInteger.def(), + Self::SelfId1 => ColumnType::BigUnsigned.def(), Self::SelfId2 => ColumnType::Integer.def(), Self::FruitId1 => ColumnType::Integer.def(), Self::FruitId2 => ColumnType::Integer.def(), diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index 0bbd8bf1..25ea2727 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -230,10 +230,14 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res let col_type = match temp { "char" => quote! { Char(None) }, "String" | "&str" => quote! { String(None) }, - "u8" | "i8" => quote! { TinyInteger }, - "u16" | "i16" => quote! { SmallInteger }, - "u32" | "i32" => quote! { Integer }, - "u64" | "i64" => quote! { BigInteger }, + "i8" => quote! { TinyInteger }, + "u8" => quote! { TinyUnsigned }, + "i16" => quote! { SmallInteger }, + "u16" => quote! { SmallUnsigned }, + "i32" => quote! { Integer }, + "u32" => quote! { Unsigned }, + "i64" => quote! { BigInteger }, + "u64" => quote! { BigUnsigned }, "f32" => quote! { Float }, "f64" => quote! { Double }, "bool" => quote! { Boolean }, diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index 1fa25757..3d062627 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -483,6 +483,71 @@ pub trait ActiveModelTrait: Clone + Debug { ActiveModelBehavior::after_delete(am_clone)?; Ok(delete_res) } + + /// Set the corresponding attributes in the ActiveModel from a JSON value + /// + /// Note that this method will not alter the primary key values in ActiveModel. + #[cfg(feature = "with-json")] + fn set_from_json(&mut self, json: serde_json::Value) -> Result<(), DbErr> + where + <::Entity as EntityTrait>::Model: IntoActiveModel, + for<'de> <::Entity as EntityTrait>::Model: + serde::de::Deserialize<'de>, + { + use crate::Iterable; + + // Backup primary key values + let primary_key_values: Vec<(::Column, ActiveValue)> = + <::PrimaryKey>::iter() + .map(|pk| (pk.into_column(), self.take(pk.into_column()))) + .collect(); + + // Replace all values in ActiveModel + *self = Self::from_json(json)?; + + // Restore primary key values + for (col, active_value) in primary_key_values { + match active_value { + ActiveValue::Unchanged(v) | ActiveValue::Set(v) => self.set(col, v), + NotSet => self.not_set(col), + } + } + + Ok(()) + } + + /// Create ActiveModel from a JSON value + #[cfg(feature = "with-json")] + fn from_json(json: serde_json::Value) -> Result + where + <::Entity as EntityTrait>::Model: IntoActiveModel, + for<'de> <::Entity as EntityTrait>::Model: + serde::de::Deserialize<'de>, + { + use crate::{Iden, Iterable}; + + // Mark down which attribute exists in the JSON object + let json_keys: Vec<(::Column, bool)> = + <::Column>::iter() + .map(|col| (col, json.get(col.to_string()).is_some())) + .collect(); + + // Convert JSON object into ActiveModel via Model + let model: ::Model = + serde_json::from_value(json).map_err(|e| DbErr::Json(e.to_string()))?; + let mut am = model.into_active_model(); + + // Transform attribute that exists in JSON object into ActiveValue::Set, otherwise ActiveValue::NotSet + for (col, json_key_exists) in json_keys { + if json_key_exists && !am.is_not_set(col) { + am.set(col, am.get(col).unwrap()); + } else { + am.not_set(col); + } + } + + Ok(am) + } } /// A Trait for overriding the ActiveModel behavior @@ -768,13 +833,15 @@ where #[cfg(test)] mod tests { - use crate::tests_cfg::*; + use crate::{entity::*, tests_cfg::*, DbErr}; + use pretty_assertions::assert_eq; + + #[cfg(feature = "with-json")] + use serde_json::json; #[test] #[cfg(feature = "macros")] fn test_derive_into_active_model_1() { - use crate::entity::*; - mod my_fruit { pub use super::fruit::*; use crate as sea_orm; @@ -806,8 +873,6 @@ mod tests { #[test] #[cfg(feature = "macros")] fn test_derive_into_active_model_2() { - use crate::entity::*; - mod my_fruit { pub use super::fruit::*; use crate as sea_orm; @@ -852,4 +917,175 @@ mod tests { } ); } + + #[test] + #[cfg(feature = "with-json")] + #[should_panic( + expected = r#"called `Result::unwrap()` on an `Err` value: Json("missing field `id`")"# + )] + fn test_active_model_set_from_json_1() { + let mut cake: cake::ActiveModel = Default::default(); + + cake.set_from_json(json!({ + "name": "Apple Pie", + })) + .unwrap(); + } + + #[test] + #[cfg(feature = "with-json")] + fn test_active_model_set_from_json_2() -> Result<(), DbErr> { + let mut fruit: fruit::ActiveModel = Default::default(); + + fruit.set_from_json(json!({ + "name": "Apple", + }))?; + assert_eq!( + fruit, + fruit::ActiveModel { + id: ActiveValue::NotSet, + name: ActiveValue::Set("Apple".to_owned()), + cake_id: ActiveValue::NotSet, + } + ); + + assert_eq!( + fruit::ActiveModel::from_json(json!({ + "name": "Apple", + }))?, + fruit::ActiveModel { + id: ActiveValue::NotSet, + name: ActiveValue::Set("Apple".to_owned()), + cake_id: ActiveValue::NotSet, + } + ); + + fruit.set_from_json(json!({ + "name": "Apple", + "cake_id": null, + }))?; + assert_eq!( + fruit, + fruit::ActiveModel { + id: ActiveValue::NotSet, + name: ActiveValue::Set("Apple".to_owned()), + cake_id: ActiveValue::Set(None), + } + ); + + fruit.set_from_json(json!({ + "id": null, + "name": "Apple", + "cake_id": 1, + }))?; + assert_eq!( + fruit, + fruit::ActiveModel { + id: ActiveValue::NotSet, + name: ActiveValue::Set("Apple".to_owned()), + cake_id: ActiveValue::Set(Some(1)), + } + ); + + fruit.set_from_json(json!({ + "id": 2, + "name": "Apple", + "cake_id": 1, + }))?; + assert_eq!( + fruit, + fruit::ActiveModel { + id: ActiveValue::NotSet, + name: ActiveValue::Set("Apple".to_owned()), + cake_id: ActiveValue::Set(Some(1)), + } + ); + + let mut fruit = fruit::ActiveModel { + id: ActiveValue::Set(1), + name: ActiveValue::NotSet, + cake_id: ActiveValue::NotSet, + }; + fruit.set_from_json(json!({ + "id": 8, + "name": "Apple", + "cake_id": 1, + }))?; + assert_eq!( + fruit, + fruit::ActiveModel { + id: ActiveValue::Set(1), + name: ActiveValue::Set("Apple".to_owned()), + cake_id: ActiveValue::Set(Some(1)), + } + ); + + Ok(()) + } + + #[smol_potat::test] + #[cfg(feature = "with-json")] + async fn test_active_model_set_from_json_3() -> Result<(), DbErr> { + use crate::*; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 1, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 1, + rows_affected: 1, + }, + ]) + .append_query_results(vec![ + vec![fruit::Model { + id: 1, + name: "Apple".to_owned(), + cake_id: None, + }], + vec![fruit::Model { + id: 2, + name: "Orange".to_owned(), + cake_id: Some(1), + }], + ]) + .into_connection(); + + let mut fruit: fruit::ActiveModel = Default::default(); + fruit.set_from_json(json!({ + "name": "Apple", + }))?; + fruit.save(&db).await?; + + let mut fruit = fruit::ActiveModel { + id: Set(2), + ..Default::default() + }; + fruit.set_from_json(json!({ + "id": 9, + "name": "Orange", + "cake_id": 1, + }))?; + fruit.save(&db).await?; + + assert_eq!( + db.into_transaction_log(), + vec![ + Transaction::from_sql_and_values( + DbBackend::Postgres, + r#"INSERT INTO "fruit" ("name") VALUES ($1) RETURNING "id", "name", "cake_id""#, + vec!["Apple".into()] + ), + Transaction::from_sql_and_values( + DbBackend::Postgres, + r#"UPDATE "fruit" SET "name" = $1, "cake_id" = $2 WHERE "fruit"."id" = $3 RETURNING "id", "name", "cake_id""#, + vec!["Orange".into(), 1i32.into(), 2i32.into()] + ), + ] + ); + + Ok(()) + } } diff --git a/src/entity/column.rs b/src/entity/column.rs index c4ea545f..cbf51475 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -30,6 +30,14 @@ pub enum ColumnType { /// `BIGINT` is a 64-bit representation of an integer taking up 8 bytes of storage and /// ranging from -2^63 (-9,223,372,036,854,775,808) to 2^63 (9,223,372,036,854,775,807). BigInteger, + /// `TINYINT UNSIGNED` data type + TinyUnsigned, + /// `SMALLINT UNSIGNED` data type + SmallUnsigned, + /// `INTEGER UNSIGNED` data type + Unsigned, + /// `BIGINT UNSIGNED` data type + BigUnsigned, /// `FLOAT` an approximate-number data type, where values range cannot be represented exactly. Float, /// `DOUBLE` is a normal-size floating point number where the @@ -362,6 +370,10 @@ impl From for sea_query::ColumnType { ColumnType::SmallInteger => sea_query::ColumnType::SmallInteger(None), ColumnType::Integer => sea_query::ColumnType::Integer(None), ColumnType::BigInteger => sea_query::ColumnType::BigInteger(None), + ColumnType::TinyUnsigned => sea_query::ColumnType::TinyUnsigned(None), + ColumnType::SmallUnsigned => sea_query::ColumnType::SmallUnsigned(None), + ColumnType::Unsigned => sea_query::ColumnType::Unsigned(None), + ColumnType::BigUnsigned => sea_query::ColumnType::BigUnsigned(None), ColumnType::Float => sea_query::ColumnType::Float(None), ColumnType::Double => sea_query::ColumnType::Double(None), ColumnType::Decimal(s) => sea_query::ColumnType::Decimal(s), @@ -395,6 +407,10 @@ impl From for ColumnType { sea_query::ColumnType::SmallInteger(_) => Self::SmallInteger, sea_query::ColumnType::Integer(_) => Self::Integer, sea_query::ColumnType::BigInteger(_) => Self::BigInteger, + sea_query::ColumnType::TinyUnsigned(_) => Self::TinyUnsigned, + sea_query::ColumnType::SmallUnsigned(_) => Self::SmallUnsigned, + sea_query::ColumnType::Unsigned(_) => Self::Unsigned, + sea_query::ColumnType::BigUnsigned(_) => Self::BigUnsigned, sea_query::ColumnType::Float(_) => Self::Float, sea_query::ColumnType::Double(_) => Self::Double, sea_query::ColumnType::Decimal(s) => Self::Decimal(s), @@ -513,13 +529,21 @@ mod tests { pub id: i32, pub one: i32, #[sea_orm(unique)] - pub two: i32, + pub two: i8, #[sea_orm(indexed)] - pub three: i32, + pub three: i16, #[sea_orm(nullable)] pub four: i32, #[sea_orm(unique, indexed, nullable)] - pub five: i32, + pub five: i64, + #[sea_orm(unique)] + pub six: u8, + #[sea_orm(indexed)] + pub seven: u16, + #[sea_orm(nullable)] + pub eight: u32, + #[sea_orm(unique, indexed, nullable)] + pub nine: u64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -529,10 +553,13 @@ mod tests { } assert_eq!(hello::Column::One.def(), ColumnType::Integer.def()); - assert_eq!(hello::Column::Two.def(), ColumnType::Integer.def().unique()); + assert_eq!( + hello::Column::Two.def(), + ColumnType::TinyInteger.def().unique() + ); assert_eq!( hello::Column::Three.def(), - ColumnType::Integer.def().indexed() + ColumnType::SmallInteger.def().indexed() ); assert_eq!( hello::Column::Four.def(), @@ -540,7 +567,23 @@ mod tests { ); assert_eq!( hello::Column::Five.def(), - ColumnType::Integer.def().unique().indexed().nullable() + ColumnType::BigInteger.def().unique().indexed().nullable() + ); + assert_eq!( + hello::Column::Six.def(), + ColumnType::TinyUnsigned.def().unique() + ); + assert_eq!( + hello::Column::Seven.def(), + ColumnType::SmallUnsigned.def().indexed() + ); + assert_eq!( + hello::Column::Eight.def(), + ColumnType::Unsigned.def().nullable() + ); + assert_eq!( + hello::Column::Nine.def(), + ColumnType::BigUnsigned.def().unique().indexed().nullable() ); } diff --git a/src/error.rs b/src/error.rs index 5289c45f..273b9c9d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,8 @@ pub enum DbErr { Custom(String), /// Error occurred while parsing value as target type Type(String), + /// Error occurred while parsing json value as target type + Json(String), } impl std::error::Error for DbErr {} @@ -26,6 +28,7 @@ impl std::fmt::Display for DbErr { Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s), Self::Custom(s) => write!(f, "Custom Error: {}", s), Self::Type(s) => write!(f, "Type Error: {}", s), + Self::Json(s) => write!(f, "Json Error: {}", s), } } } diff --git a/src/lib.rs b/src/lib.rs index cf609cea..892b397a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ //! //! ## Getting Started //! -//! [![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/SeaQL/sea-orm/stargazers/) +//! [![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) //! If you like what we do, consider starring, commenting, sharing and contributing! //! //! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) @@ -37,6 +37,7 @@ //! + [Rocket Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) //! + [Actix Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) //! + [Axum Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) +//! + [Axum-GraphQL Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum-graphql_example) //! + [Poem Example](https://github.com/SeaQL/sea-orm/tree/master/examples/poem_example) //! //! ## Features diff --git a/src/tests_cfg/cake.rs b/src/tests_cfg/cake.rs index 8c01bf16..eb01ed5c 100644 --- a/src/tests_cfg/cake.rs +++ b/src/tests_cfg/cake.rs @@ -1,7 +1,11 @@ use crate as sea_orm; use crate::entity::prelude::*; +#[cfg(feature = "with-json")] +use serde::{Deserialize, Serialize}; + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[cfg_attr(feature = "with-json", derive(Serialize, Deserialize))] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/src/tests_cfg/fruit.rs b/src/tests_cfg/fruit.rs index 166820a2..0ad7757e 100644 --- a/src/tests_cfg/fruit.rs +++ b/src/tests_cfg/fruit.rs @@ -1,10 +1,15 @@ use crate as sea_orm; use crate::entity::prelude::*; +#[cfg(feature = "with-json")] +use serde::{Deserialize, Serialize}; + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[cfg_attr(feature = "with-json", derive(Serialize, Deserialize))] #[sea_orm(table_name = "fruit")] pub struct Model { #[sea_orm(primary_key)] + #[serde(skip_deserializing)] pub id: i32, pub name: String, pub cake_id: Option,