From 7e26cac1fe2c738531f51bd129694cb58d5b3b0c Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 4 Sep 2025 16:02:55 +0100 Subject: [PATCH 01/85] Bump sea-orm & sea-query --- Cargo.toml | 9 +++---- README.md | 1 + cli/Cargo.toml | 4 +-- examples/mysql/Cargo.toml | 6 ++--- examples/postgres/Cargo.toml | 8 +++--- examples/sqlite/Cargo.toml | 6 ++--- generator/Cargo.toml | 3 +-- generator/src/templates/actix_cargo.toml | 2 +- generator/src/templates/axum_cargo.toml | 2 +- generator/src/templates/poem_cargo.toml | 2 +- src/builder.rs | 2 +- src/builder_context/filter_types_map.rs | 10 +++---- src/builder_context/types_map.rs | 34 ++++++++++++------------ src/lib.rs | 1 + src/query/loader.rs | 32 +++++++++++++--------- src/query/pagination.rs | 11 ++++---- src/utilities.rs | 9 +++---- 17 files changed, 73 insertions(+), 69 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 49bbc20a..85a2bc96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "1.1.4" +version = "2.0.0-rc.1" edition = "2021" rust-version = "1.70" authors = [ @@ -20,7 +20,7 @@ categories = ["database"] [dependencies] async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } -sea-orm = { version = "1.1.15", default-features = false, features = ["seaography", "with-json"] } +sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } seaography-macros = { version = "0.1.0", path = "macros", optional = true } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } @@ -48,6 +48,5 @@ field-camel-case = [] field-pluralize = ["pluralizer"] strict-custom-types = ["seaography-macros/strict-custom-types"] -# [patch.crates-io] -# sea-orm = { git = "https://github.com/SeaQL/sea-orm" } -# sea-orm-migration = { git = "https://github.com/SeaQL/sea-orm" } +[patch.crates-io] +sea-orm = { git = "https://github.com/SeaQL/sea-orm", branch = "master" } diff --git a/README.md b/README.md index 2fb882a1..c0d392b4 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ It's easy to integrate Seaography with any web framework, but we ship with the f | Seaography | SeaORM | |----------------------------------------------------------|-------------------------------------------------------| +| [2.0](https://crates.io/crates/seaography/2.0.0-rc) | [2.0](https://crates.io/crates/sea-orm/2.0.0-rc) | | [1.1](https://crates.io/crates/seaography/1.1.4) | [1.1](https://crates.io/crates/sea-orm/1.1.13) | ## Quick start - ready to serve in 3 minutes! diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 9415bc52..7222e5e5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-cli" -version = "1.1.4" +version = "2.0.0-rc.1" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } clap = { version = "4.3.19", features = ["derive"] } -seaography-generator = { version = "~1.1.4", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc.1", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index fa8e0c9a..d7e1bc7a 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -1,20 +1,20 @@ [package] edition = "2021" name = "seaography-mysql-example" -version = "1.1.4" +version = "2.0.0-rc.1" [dependencies] axum = { version = "0.7" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~1.1.5", features = ["sqlx-mysql", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["sqlx-mysql", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~1.1.4" # seaography version +version = "~2.0.0-rc.1" # seaography version features = ["with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index b9fb360d..2c0d820f 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,24 +1,24 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "1.1.4" +version = "2.0.0-rc.1" [dependencies] poem = { version = "3.0" } async-graphql-poem = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~1.1.14", features = ["sqlx-postgres", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["sqlx-postgres", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~1.1.4" # seaography version +version = "~2.0.0-rc.1" # seaography version features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array"] [dev-dependencies] serde_json = { version = "1.0.103" } [workspace] -members = [] \ No newline at end of file +members = [] diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 0a514b01..201be54a 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,14 +1,14 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "1.1.4" +version = "2.0.0-rc.1" [dependencies] actix-web = { version = "4.5", default-features = false, features = ["macros"] } async-graphql = { version = "7.0", features = ["dataloader", "dynamic-schema"] } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~1.1.14", features = ["sqlx-sqlite", "runtime-async-std-rustls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["sqlx-sqlite", "runtime-async-std-rustls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } @@ -17,7 +17,7 @@ serde_json = { version = "1.0.103" } [dependencies.seaography] path = "../../" -version = "~1.1.4" # seaography version +version = "~2.0.0-rc.1" # seaography version features = ["macros", "with-decimal", "with-chrono"] [workspace] diff --git a/generator/Cargo.toml b/generator/Cargo.toml index 467b29c1..e723c356 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-generator" -version = "1.1.4" +version = "2.0.0-rc.1" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -18,5 +18,4 @@ proc-macro2 = "1.0.66" syn = { version = "2.0.27", features = ["full"] } heck = "0.4.1" itertools = "0.11.0" -sea-query = { version = "~0.32.0", default-features = false } thiserror = "1.0.44" \ No newline at end of file diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index 8cf3b723..fc3395c3 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" actix-web = { version = "4.5", default-features = false, features = ["macros"] } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~1.1.5", features = ["", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/generator/src/templates/axum_cargo.toml b/generator/src/templates/axum_cargo.toml index 98491d21..57edee9e 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" axum = { version = "0.7" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~1.1.5", features = ["", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/generator/src/templates/poem_cargo.toml b/generator/src/templates/poem_cargo.toml index f67cc49a..7004eaff 100644 --- a/generator/src/templates/poem_cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" poem = { version = "3.0" } async-graphql-poem = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~1.1.5", features = ["", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/src/builder.rs b/src/builder.rs index 225db0c9..48ff9321 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,7 +5,7 @@ use async_graphql::{ SubscriptionField, TypeRef, Union, }, }; -use sea_orm::{ActiveEnum, ActiveModelTrait, ConnectionTrait, EntityTrait, IntoActiveModel}; +use sea_orm::{ActiveEnum, ActiveModelTrait, EntityTrait, IntoActiveModel}; use crate::{ ActiveEnumBuilder, ActiveEnumFilterInputBuilder, BuilderContext, ConnectionObjectBuilder, diff --git a/src/builder_context/filter_types_map.rs b/src/builder_context/filter_types_map.rs index 5f55be8c..eb1a0623 100644 --- a/src/builder_context/filter_types_map.rs +++ b/src/builder_context/filter_types_map.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use async_graphql::dynamic::{InputObject, InputValue, ObjectAccessor, TypeRef}; -use sea_orm::{ColumnTrait, ColumnType, Condition, EntityTrait}; +use sea_orm::{ColumnTrait, ColumnType, Condition, EntityTrait, ExprTrait}; use crate::{ prepare_enumeration_condition, ActiveEnumFilterInputBuilder, BuilderContext, @@ -661,14 +661,12 @@ impl FilterTypesMapHelper { } } FilterOperation::CaseInsensitiveEquals => { - use sea_orm::sea_query::{Expr, ExprTrait, Func, SimpleExpr}; + use sea_orm::sea_query::{Expr, Func}; if let Some(value) = filter.get("ci_eq") { let value = types_map_helper .async_graphql_value_to_sea_orm_value::(column, &value)?; - condition = condition.add( - Func::lower(Expr::col(*column)) - .eq(SimpleExpr::FunctionCall(Func::lower(value))), - ); + condition = + condition.add(Func::lower(Expr::col(*column)).eq(Func::lower(value))); } } FilterOperation::IsIn => { diff --git a/src/builder_context/types_map.rs b/src/builder_context/types_map.rs index 85fc26a2..e6111437 100644 --- a/src/builder_context/types_map.rs +++ b/src/builder_context/types_map.rs @@ -632,7 +632,7 @@ pub fn converted_value_to_sea_orm_value( } ConvertedType::String | ConvertedType::Enum(_) | ConvertedType::Custom(_) => { let value = value.string()?; - sea_orm::Value::String(Some(Box::new(value.to_string()))) + sea_orm::Value::String(Some(value.to_string())) } ConvertedType::Char => { let value = value.string()?; @@ -644,7 +644,7 @@ pub fn converted_value_to_sea_orm_value( } ConvertedType::Bytes => { let value = decode_hex(value.string()?)?; - sea_orm::Value::Bytes(Some(Box::new(value))) + sea_orm::Value::Bytes(Some(value)) } #[cfg(feature = "with-json")] ConvertedType::Json => { @@ -658,7 +658,7 @@ pub fn converted_value_to_sea_orm_value( })?, Err(_) => value.deserialize()?, }; - sea_orm::Value::Json(Some(Box::new(value))) + sea_orm::Value::Json(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDate => { @@ -671,7 +671,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::ChronoDate(Some(Box::new(value))) + sea_orm::Value::ChronoDate(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoTime => { @@ -684,7 +684,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::ChronoTime(Some(Box::new(value))) + sea_orm::Value::ChronoTime(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTime => { @@ -699,7 +699,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::ChronoDateTime(Some(Box::new(value))) + sea_orm::Value::ChronoDateTime(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTimeUtc => { @@ -713,7 +713,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::ChronoDateTimeUtc(Some(Box::new(value))) + sea_orm::Value::ChronoDateTimeUtc(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTimeLocal => { @@ -727,7 +727,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::ChronoDateTimeLocal(Some(Box::new(value))) + sea_orm::Value::ChronoDateTimeLocal(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTimeWithTimeZone => { @@ -745,7 +745,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::ChronoDateTimeWithTimeZone(Some(Box::new(value))) + sea_orm::Value::ChronoDateTimeWithTimeZone(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeDate => { @@ -760,7 +760,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::TimeDate(Some(Box::new(value))) + sea_orm::Value::TimeDate(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeTime => { @@ -775,7 +775,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::TimeTime(Some(Box::new(value))) + sea_orm::Value::TimeTime(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeDateTime => { @@ -790,7 +790,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::TimeDateTime(Some(Box::new(value))) + sea_orm::Value::TimeDateTime(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeDateTimeWithTimeZone => { @@ -805,7 +805,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::TimeDateTimeWithTimeZone(Some(Box::new(value))) + sea_orm::Value::TimeDateTimeWithTimeZone(Some(value)) } #[cfg(feature = "with-uuid")] ConvertedType::Uuid => { @@ -818,7 +818,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::Uuid(Some(Box::new(value))) + sea_orm::Value::Uuid(Some(value)) } #[cfg(feature = "with-decimal")] ConvertedType::Decimal => { @@ -832,7 +832,7 @@ pub fn converted_value_to_sea_orm_value( ) })?; - sea_orm::Value::Decimal(Some(Box::new(value))) + sea_orm::Value::Decimal(Some(value)) } #[cfg(feature = "with-bigdecimal")] ConvertedType::BigDecimal => { @@ -864,13 +864,13 @@ pub fn converted_value_to_sea_orm_value( // #[cfg(feature = "with-ipnetwork")] // ConvertedType::IpNetwork => { // let value = value.string()?; - // sea_orm::Value::String(Some(Box::new(value.to_string()))) + // sea_orm::Value::String(Some(value.to_string())) // } // FIXME: support mac type // #[cfg(feature = "with-mac_address")] // ConvertedType::MacAddress => { // let value = value.string()?; - // sea_orm::Value::String(Some(Box::new(value.to_string()))) + // sea_orm::Value::String(Some(value.to_string())) // } }) } diff --git a/src/lib.rs b/src/lib.rs index 5d78b7b1..6f0722ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,7 @@ //! //! | Seaography | SeaORM | //! |----------------------------------------------------------|-------------------------------------------------------| +//! | [2.0](https://crates.io/crates/seaography/2.0.0-rc) | [2.0](https://crates.io/crates/sea-orm/2.0.0-rc) | //! | [1.1](https://crates.io/crates/seaography/1.1.4) | [1.1](https://crates.io/crates/sea-orm/1.1.13) | //! //! ## Quick start - ready to serve in 3 minutes! diff --git a/src/query/loader.rs b/src/query/loader.rs index a9422b07..8d00f56e 100644 --- a/src/query/loader.rs +++ b/src/query/loader.rs @@ -1,4 +1,4 @@ -use sea_orm::{sea_query::ValueTuple, Condition, ModelTrait, QueryFilter}; +use sea_orm::{sea_query::ValueTuple, Condition, ExprTrait, ModelTrait, QueryFilter}; use std::{collections::HashMap, hash::Hash, marker::PhantomData, sync::Arc}; use crate::apply_order; @@ -118,8 +118,14 @@ where { fn eq(&self, other: &Self) -> bool { self.filters.eq(&other.filters) - && format!("{:?}", self.columns).eq(&format!("{:?}", other.columns)) - && format!("{:?}", self.order_by).eq(&format!("{:?}", other.order_by)) + && std::cmp::PartialEq::eq( + &format!("{:?}", self.columns), + &format!("{:?}", other.columns), + ) + && std::cmp::PartialEq::eq( + &format!("{:?}", self.order_by), + &format!("{:?}", other.order_by), + ) } } @@ -146,7 +152,7 @@ where T: sea_orm::EntityTrait, { fn eq(&self, other: &Self) -> bool { - format!("{:?}", self.0).eq(&format!("{:?}", other.0)) + std::cmp::PartialEq::eq(&format!("{:?}", self.0), &format!("{:?}", other.0)) } } @@ -237,10 +243,11 @@ where Some(condition) => Condition::all().add(condition), None => Condition::all(), }; - let tuple = - sea_orm::sea_query::Expr::tuple(key.columns.iter().map( - |column: &T::Column| sea_orm::sea_query::Expr::col(*column).into(), - )); + let tuple = sea_orm::sea_query::Expr::tuple( + key.columns + .iter() + .map(|column: &T::Column| sea_orm::sea_query::Expr::col(*column)), + ); let condition = condition.add(tuple.in_tuples(values.into_iter().map(ValueTuple::Many))); let stmt = stmt.filter(condition); @@ -357,10 +364,11 @@ where Some(condition) => Condition::all().add(condition), None => Condition::all(), }; - let tuple = - sea_orm::sea_query::Expr::tuple(key.columns.iter().map( - |column: &T::Column| sea_orm::sea_query::Expr::col(*column).into(), - )); + let tuple = sea_orm::sea_query::Expr::tuple( + key.columns + .iter() + .map(|column: &T::Column| sea_orm::sea_query::Expr::col(*column)), + ); let condition = condition.add(tuple.in_tuples(values.into_iter().map(ValueTuple::Many))); let stmt = stmt.filter(condition); diff --git a/src/query/pagination.rs b/src/query/pagination.rs index 5c984b97..f79d5e6d 100644 --- a/src/query/pagination.rs +++ b/src/query/pagination.rs @@ -204,13 +204,12 @@ where let start_cursor = edges.first().map(|edge| edge.cursor.clone()); let end_cursor = edges.last().map(|edge| edge.cursor.clone()); - let count_stmt = db.get_database_backend().build( - sea_orm::sea_query::SelectStatement::new() - .expr(sea_orm::sea_query::Expr::cust("COUNT(*) AS num_items")) - .from_subquery(count_stmt, sea_orm::sea_query::Alias::new("sub_query")), - ); + let count_query = sea_orm::sea_query::SelectStatement::new() + .expr(sea_orm::sea_query::Expr::cust("COUNT(*) AS num_items")) + .from_subquery(count_stmt, sea_orm::sea_query::Alias::new("sub_query")) + .take(); - let total = match db.query_one(count_stmt).await? { + let total = match db.query_one(&count_query).await? { Some(res) => match db.get_database_backend() { sea_orm::DbBackend::Postgres => res.try_get::("", "num_items")? as u64, _ => res.try_get::("", "num_items")? as u64, diff --git a/src/utilities.rs b/src/utilities.rs index e1c339be..23abdb17 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -75,7 +75,6 @@ pub fn encode_cursor(values: Vec) -> String { } sea_orm::Value::String(value) => { if let Some(value) = value { - let value = value.as_ref(); format!("String[{}]:{}", value.len(), value) } else { "String[-1]:".into() @@ -84,7 +83,7 @@ pub fn encode_cursor(values: Vec) -> String { #[cfg(feature = "with-uuid")] sea_orm::Value::Uuid(value) => { if let Some(value) = value { - let value = value.as_ref().to_string(); + let value = value.to_string(); format!("Uuid[{}]:{}", value.len(), value) } else { "Uuid[-1]:".into() @@ -246,7 +245,7 @@ pub fn decode_cursor(s: &str) -> Result, sea_orm::DbErr> { if length.eq(&-1) { sea_orm::Value::String(None) } else { - sea_orm::Value::String(Some(Box::new(data_buffer))) + sea_orm::Value::String(Some(data_buffer)) } } #[cfg(feature = "with-uuid")] @@ -254,11 +253,11 @@ pub fn decode_cursor(s: &str) -> Result, sea_orm::DbErr> { if length.eq(&-1) { sea_orm::Value::Uuid(None) } else { - sea_orm::Value::Uuid(Some(Box::new( + sea_orm::Value::Uuid(Some( data_buffer.parse::().map_err(|e| { sea_orm::DbErr::Type(format!("Failed to parse UUID: {e}")) })?, - ))) + )) } } ty => { From 468392d429cd41ab3e0b6ccf28cca85d3630fbdc Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 24 Aug 2025 11:10:14 +0100 Subject: [PATCH 02/85] Support SeaORM RBAC (#212) * Support rbac: unrestricted by default * Use generic inplace of RestrictedConnection in public API * guard behind feature flag `rbac` --- .github/workflows/tests.yaml | 1 + Cargo.toml | 1 + examples/sqlite/Cargo.toml | 1 + examples/sqlite/tests/query_tests.rs | 4 +- src/lib.rs | 3 + src/mutation/entity_create_batch_mutation.rs | 10 ++- src/mutation/entity_create_one_mutation.rs | 9 ++- src/mutation/entity_delete_mutation.rs | 9 ++- src/mutation/entity_update_mutation.rs | 9 ++- src/query/entity_object_relation.rs | 22 +++++-- src/query/entity_object_via_relation.rs | 24 ++++++-- src/query/entity_query_field.rs | 12 ++-- src/query/pagination.rs | 9 +-- src/rbac.rs | 65 ++++++++++++++++++++ 14 files changed, 147 insertions(+), 32 deletions(-) create mode 100644 src/rbac.rs diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 78d72756..e3986034 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -62,6 +62,7 @@ jobs: run: | cargo test --test custom_query_tests cargo test --test custom_mutation_tests + cargo test --test query_tests --features=rbac cargo test --test plural_query_tests --features=field-pluralize cargo test --test entity_filter_tests --features=field-pluralize rm tests/custom_mutation_tests.rs tests/custom_query_tests.rs tests/plural_query_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 85a2bc96..251a7261 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ time = { version = "0.3", features = ["formatting"], optional = true } [features] default = ["field-camel-case"] macros = ["seaography-macros"] +rbac = ["sea-orm/rbac"] with-json = ["sea-orm/with-json"] with-chrono = ["sea-orm/with-chrono", "async-graphql/chrono"] with-time = ["sea-orm/with-time", "async-graphql/time", "time"] diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 201be54a..6d78ed83 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -24,6 +24,7 @@ features = ["macros", "with-decimal", "with-chrono"] members = [] [features] +rbac = ["seaography/rbac"] field-pluralize = ["seaography/field-pluralize"] [[test]] diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index d429cfaa..94d3b9a4 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -1,10 +1,10 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; -use seaography::async_graphql; +use seaography::{async_graphql, DatabaseContext}; async fn schema() -> Schema { let database = Database::connect("sqlite://sakila.db").await.unwrap(); - seaography_sqlite_example::query_root::schema(database, None, None).unwrap() + seaography_sqlite_example::query_root::schema(database.unrestricted(), None, None).unwrap() } fn assert_eq(a: Response, b: &str) { diff --git a/src/lib.rs b/src/lib.rs index 6f0722ca..b9ba4389 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -297,6 +297,9 @@ pub use builder::*; pub mod error; pub use error::*; +pub mod rbac; +pub use rbac::*; + pub type SimpleNamingFn = Box String + Sync + Send>; pub type ComplexNamingFn = Box String + Sync + Send>; diff --git a/src/mutation/entity_create_batch_mutation.rs b/src/mutation/entity_create_batch_mutation.rs index 76423a31..d69a4011 100644 --- a/src/mutation/entity_create_batch_mutation.rs +++ b/src/mutation/entity_create_batch_mutation.rs @@ -4,8 +4,9 @@ use sea_orm::{ }; use crate::{ - apply_guard, guard_error, prepare_active_model, BuilderContext, EntityInputBuilder, - EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, + apply_guard, guard_error, prepare_active_model, BuilderContext, DatabaseContext, + EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, + UserContext, }; /// The configuration structure of EntityCreateBatchMutationBuilder @@ -90,7 +91,10 @@ impl EntityCreateBatchMutationBuilder { return Err(guard_error(reason, "Entity guard triggered.")); } - let db = ctx.data::()?; + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; + let transaction = db.begin().await?; let entity_input_builder = EntityInputBuilder { context }; diff --git a/src/mutation/entity_create_one_mutation.rs b/src/mutation/entity_create_one_mutation.rs index 1b31ea3c..c17755cc 100644 --- a/src/mutation/entity_create_one_mutation.rs +++ b/src/mutation/entity_create_one_mutation.rs @@ -5,8 +5,8 @@ use sea_orm::{ }; use crate::{ - apply_guard, guard_error, BuilderContext, EntityInputBuilder, EntityObjectBuilder, - EntityQueryFieldBuilder, GuardAction, OperationType, + apply_guard, guard_error, BuilderContext, DatabaseContext, EntityInputBuilder, + EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityCreateOneMutationBuilder @@ -93,7 +93,6 @@ impl EntityCreateOneMutationBuilder { let entity_input_builder = EntityInputBuilder { context }; let entity_object_builder = EntityObjectBuilder { context }; - let db = ctx.data::()?; let value_accessor = ctx .args .try_get(&context.entity_create_one_mutation.data_field)?; @@ -115,6 +114,10 @@ impl EntityCreateOneMutationBuilder { } } + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; + let active_model = prepare_active_model::( &entity_input_builder, &entity_object_builder, diff --git a/src/mutation/entity_delete_mutation.rs b/src/mutation/entity_delete_mutation.rs index 63532741..0e04fa57 100644 --- a/src/mutation/entity_delete_mutation.rs +++ b/src/mutation/entity_delete_mutation.rs @@ -4,8 +4,9 @@ use sea_orm::{ }; use crate::{ - apply_guard, get_filter_conditions, guard_error, BuilderContext, EntityObjectBuilder, - EntityQueryFieldBuilder, FilterInputBuilder, GuardAction, OperationType, + apply_guard, get_filter_conditions, guard_error, BuilderContext, DatabaseContext, + EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, GuardAction, OperationType, + UserContext, }; /// The configuration structure of EntityDeleteMutationBuilder @@ -90,7 +91,9 @@ impl EntityDeleteMutationBuilder { return Err(guard_error(reason, "Entity guard triggered.")); } - let db = ctx.data::()?; + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; let filters = ctx.args.get(&context.entity_delete_mutation.filter_field); let filter_condition = get_filter_conditions::(context, filters)?; diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs index 438a6397..fdbc33d4 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -6,8 +6,8 @@ use sea_orm::{ use crate::{ apply_guard, get_filter_conditions, guard_error, prepare_active_model, BuilderContext, - EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, - GuardAction, OperationType, + DatabaseContext, EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, + FilterInputBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityUpdateMutationBuilder @@ -101,7 +101,10 @@ impl EntityUpdateMutationBuilder { return Err(guard_error(reason, "Entity guard triggered.")); } - let db = ctx.data::()?; + let db = ctx + .data::()? + .restricted(ctx.data_opt::())?; + let transaction = db.begin().await?; let entity_input_builder = EntityInputBuilder { context }; diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index 8e08f3f1..7040893a 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -3,13 +3,15 @@ use async_graphql::{ dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}, }; use heck::{ToLowerCamelCase, ToSnakeCase}; -use sea_orm::{EntityTrait, Iden, ModelTrait, QueryFilter, RelationDef}; +use sea_orm::{ + DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, QueryTrait, RelationDef, +}; use crate::{ apply_guard, apply_memory_pagination, get_filter_conditions, guard_error, pluralize_unique, - BuilderContext, Connection, ConnectionObjectBuilder, EntityObjectBuilder, FilterInputBuilder, - GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, OperationType, - OrderInputBuilder, PaginationInputBuilder, + BuilderContext, Connection, ConnectionObjectBuilder, DatabaseContext, EntityObjectBuilder, + FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, + OperationType, OrderInputBuilder, PaginationInputBuilder, UserContext, }; /// This builder produces a GraphQL field for an SeaORM entity relationship @@ -105,6 +107,12 @@ impl EntityObjectRelationBuilder { stmt = stmt.filter(filter); } + let db = ctx + .data::()? + .restricted(ctx.data_opt::())?; + + db.user_can_run(stmt.as_query())?; + let filters = ctx.args.get(&context.entity_query_field.filters); let filters = get_filter_conditions::(context, filters)?; let order_by = ctx.args.get(&context.entity_query_field.order_by); @@ -167,6 +175,12 @@ impl EntityObjectRelationBuilder { stmt = stmt.filter(filter); } + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; + + db.user_can_run(stmt.as_query())?; + let filters = ctx.args.get(&context.entity_query_field.filters); let filters = get_filter_conditions::(context, filters)?; let order_by = ctx.args.get(&context.entity_query_field.order_by); diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 135b8a08..8754d26c 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -4,14 +4,16 @@ use async_graphql::{ }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ - ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, Related, + ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, + QueryTrait, Related, }; use crate::{ apply_guard, apply_memory_pagination, apply_order, apply_pagination, get_filter_conditions, - guard_error, pluralize_unique, BuilderContext, ConnectionObjectBuilder, EntityObjectBuilder, - FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, - OperationType, OrderInputBuilder, PaginationInputBuilder, + guard_error, pluralize_unique, BuilderContext, ConnectionObjectBuilder, DatabaseContext, + EntityObjectBuilder, FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, + OneToManyLoader, OneToOneLoader, OperationType, OrderInputBuilder, PaginationInputBuilder, + UserContext, }; /// This builder produces a GraphQL field for an SeaORM entity related trait @@ -120,6 +122,12 @@ impl EntityObjectViaRelationBuilder { stmt = stmt.filter(filter); } + let db = ctx + .data::()? + .restricted(ctx.data_opt::())?; + + db.user_can_run(stmt.as_query())?; + let filters = ctx.args.get(&context.entity_query_field.filters); let filters = get_filter_conditions::(context, filters)?; let order_by = ctx.args.get(&context.entity_query_field.order_by); @@ -196,7 +204,9 @@ impl EntityObjectViaRelationBuilder { let pagination = PaginationInputBuilder { context }.parse_object(pagination)?; - let db = ctx.data::()?; + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; let connection = if is_via_relation { // TODO optimize query @@ -204,10 +214,12 @@ impl EntityObjectViaRelationBuilder { let stmt = stmt.filter(condition.add(filters)); let stmt = apply_order(stmt, order_by); - apply_pagination::(context, db, stmt, pagination).await? + apply_pagination::(context, db, stmt, pagination).await? } else { let loader = ctx.data_unchecked::>>(); + db.user_can_run(stmt.as_query())?; + let key = KeyComplex:: { key: vec![parent.get(from_col)], meta: HashableGroupKey:: { diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index 231adf14..7b64cb79 100644 --- a/src/query/entity_query_field.rs +++ b/src/query/entity_query_field.rs @@ -6,7 +6,7 @@ use crate::{ apply_guard, apply_order, apply_pagination, get_filter_conditions, guard_error, pluralize_unique, BuilderContext, ConnectionObjectBuilder, EntityColumnId, EntityObjectBuilder, FilterInputBuilder, GuardAction, OperationType, OrderInputBuilder, PaginationInput, - PaginationInputBuilder, + PaginationInputBuilder,DatabaseContext,UserContext }; /// The configuration structure for EntityQueryFieldBuilder @@ -143,7 +143,9 @@ impl EntityQueryFieldBuilder { )?; stmt = stmt.filter(column.eq(v)); - let db = ctx.data::()?; + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; let r = stmt.one(db).await?; @@ -212,9 +214,11 @@ impl EntityQueryFieldBuilder { stmt = stmt.filter(filters); stmt = apply_order(stmt, order_by); - let db = ctx.data::()?; + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; - let connection = apply_pagination::(context, db, stmt, pagination).await?; + let connection = apply_pagination::(context, db, stmt, pagination).await?; Ok(Some(FieldValue::owned_any(connection))) }) diff --git a/src/query/pagination.rs b/src/query/pagination.rs index f79d5e6d..76e48892 100644 --- a/src/query/pagination.rs +++ b/src/query/pagination.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use sea_orm::{ - ConnectionTrait, DatabaseConnection, EntityTrait, Iterable, ModelTrait, PaginatorTrait, - PrimaryKeyArity, PrimaryKeyToColumn, PrimaryKeyTrait, QuerySelect, QueryTrait, Select, + ConnectionTrait, EntityTrait, Iterable, ModelTrait, PaginatorTrait, PrimaryKeyArity, + PrimaryKeyToColumn, PrimaryKeyTrait, QuerySelect, QueryTrait, Select, }; use crate::{ @@ -10,15 +10,16 @@ use crate::{ }; /// used to parse pagination input object and apply it to statement -pub async fn apply_pagination( +pub async fn apply_pagination( context: &'static BuilderContext, - db: &DatabaseConnection, + db: &C, stmt: Select, pagination: PaginationInput, ) -> Result, sea_orm::DbErr> where T: EntityTrait, ::Model: Sync, + C: ConnectionTrait, { let pagination = apply_pagination_defaults(context, pagination); diff --git a/src/rbac.rs b/src/rbac.rs new file mode 100644 index 00000000..15e5e108 --- /dev/null +++ b/src/rbac.rs @@ -0,0 +1,65 @@ +use sea_orm::{DatabaseConnection, DbErr, StatementBuilder}; + +#[derive(Default)] +pub struct UserContext { + pub user_id: i64, +} + +pub trait DatabaseContext { + type Connection; + + fn unrestricted(self) -> Self; + + fn restricted(&self, user_ctx: Option<&UserContext>) -> Result; + + fn user_can_run(&self, stmt: &S) -> Result<(), DbErr>; +} + +#[cfg(feature = "rbac")] +impl DatabaseContext for DatabaseConnection { + type Connection = sea_orm::RestrictedConnection; + + fn unrestricted(self) -> Self { + use sea_orm::rbac::{RbacEngine, RbacSnapshot}; + + self.replace_rbac(RbacEngine::from_snapshot( + RbacSnapshot::danger_unrestricted(), + )); + self + } + + fn restricted( + &self, + user_ctx: Option<&UserContext>, + ) -> Result { + use sea_orm::rbac::RbacUserId; + + self.restricted_for(match user_ctx { + Some(user_ctx) => RbacUserId(user_ctx.user_id), + None => RbacUserId(0), + }) + } + + fn user_can_run(&self, _: &S) -> Result<(), DbErr> { + Err(DbErr::RbacError(format!( + "feature `rbac` is enabled, can only query through RestrictedConnection" + ))) + } +} + +#[cfg(not(feature = "rbac"))] +impl DatabaseContext for DatabaseConnection { + type Connection = DatabaseConnection; + + fn unrestricted(self) -> Self { + self + } + + fn restricted(&self, _: Option<&UserContext>) -> Result { + Ok(self.clone()) + } + + fn user_can_run(&self, _: &S) -> Result<(), DbErr> { + Ok(()) + } +} From accee100ca6061fbd7c38069d6da04d5d6062826 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 2 Sep 2025 16:44:34 +0100 Subject: [PATCH 03/85] Convert schema meta to GraphQL schema --- .github/workflows/tests.yaml | 4 +- Cargo.toml | 4 +- examples/postgres/Cargo.toml | 2 +- .../postgres/tests/entity_metadata_tests.rs | 256 ++++++++++++++++++ src/builder.rs | 68 +++-- src/lib.rs | 3 + src/outputs/entity_object.rs | 2 +- src/query/entity_query_field.rs | 6 +- src/schema.rs | 244 +++++++++++++++++ 9 files changed, 563 insertions(+), 26 deletions(-) create mode 100644 examples/postgres/tests/entity_metadata_tests.rs create mode 100644 src/schema.rs diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e3986034..4171986d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -195,8 +195,8 @@ jobs: - name: Additional Integration tests working-directory: ./examples/postgres run: | - cargo test --test pg_query_tests - rm tests/pg_query_tests.rs + cargo test --test pg_query_tests entity_metadata_tests + rm tests/pg_query_tests.rs tests/entity_metadata_tests.rs - name: Remove generated folder run: rm -rf ./examples/postgres/src - name: Generate entities diff --git a/Cargo.toml b/Cargo.toml index 251a7261..173b1445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,13 +27,15 @@ heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } fnv = { version = "1.0.7" } lazy_static = { version = "1.5" } +serde = { version = "1.0", optional = true } serde_json = { version = "1.0" } pluralizer = { version = "0.5", optional = true } time = { version = "0.3", features = ["formatting"], optional = true } [features] -default = ["field-camel-case"] +default = ["field-camel-case", "schema-meta"] macros = ["seaography-macros"] +schema-meta = ["macros", "serde/derive", "with-postgres-array"] rbac = ["sea-orm/rbac"] with-json = ["sea-orm/with-json"] with-chrono = ["sea-orm/with-chrono", "async-graphql/chrono"] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 2c0d820f..9f593169 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -15,7 +15,7 @@ tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" version = "~2.0.0-rc.1" # seaography version -features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array"] +features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] serde_json = { version = "1.0.103" } diff --git a/examples/postgres/tests/entity_metadata_tests.rs b/examples/postgres/tests/entity_metadata_tests.rs new file mode 100644 index 00000000..f6d9c433 --- /dev/null +++ b/examples/postgres/tests/entity_metadata_tests.rs @@ -0,0 +1,256 @@ +use async_graphql::{dynamic::*, Response}; +use sea_orm::Database; +use seaography::async_graphql; + +pub async fn get_schema() -> Schema { + let database = Database::connect("postgres://sea:sea@127.0.0.1/sakila") + .await + .unwrap(); + let schema = seaography_postgres_example::query_root::schema(database, None, None).unwrap(); + + schema +} + +pub fn assert_eq(a: Response, b: &str) { + assert_eq!( + a.data.into_json().unwrap(), + serde_json::from_str::(b).unwrap() + ) +} + +#[tokio::test] +async fn test_entity_metadata() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#"{ + city: _sea_orm_entity_metadata(table_name: "city") { + columns { + name + nullable + type_ { + primitive + array { + array { + primitive + } + } + enumeration { + name + variants + } + } + } + primary_key + } + film: _sea_orm_entity_metadata(table_name: "film") { + columns { + name + nullable + type_ { + primitive + array { + array { + primitive + } + } + enumeration { + name + variants + } + } + } + primary_key + } + } + "#, + ) + .await, + r#" + { + "city": { + "columns": [ + { + "name": "city_id", + "nullable": false, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "city", + "nullable": false, + "type_": { + "primitive": "string", + "array": null, + "enumeration": null + } + }, + { + "name": "country_id", + "nullable": false, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "last_update", + "nullable": false, + "type_": { + "primitive": "datetime", + "array": null, + "enumeration": null + } + } + ], + "primary_key": [ + "city_id" + ] + }, + "film": { + "columns": [ + { + "name": "film_id", + "nullable": false, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "title", + "nullable": false, + "type_": { + "primitive": "string", + "array": null, + "enumeration": null + } + }, + { + "name": "description", + "nullable": true, + "type_": { + "primitive": "string", + "array": null, + "enumeration": null + } + }, + { + "name": "release_year", + "nullable": true, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "language_id", + "nullable": false, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "original_language_id", + "nullable": true, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "rental_duration", + "nullable": false, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "rental_rate", + "nullable": false, + "type_": { + "primitive": "decimal", + "array": null, + "enumeration": null + } + }, + { + "name": "length", + "nullable": true, + "type_": { + "primitive": "integer", + "array": null, + "enumeration": null + } + }, + { + "name": "replacement_cost", + "nullable": false, + "type_": { + "primitive": "decimal", + "array": null, + "enumeration": null + } + }, + { + "name": "rating", + "nullable": true, + "type_": { + "primitive": null, + "array": null, + "enumeration": { + "name": "mpaa_rating", + "variants": [ + "G", + "NC-17", + "PG", + "PG-13", + "R" + ] + } + } + }, + { + "name": "last_update", + "nullable": false, + "type_": { + "primitive": "datetime", + "array": null, + "enumeration": null + } + }, + { + "name": "special_features", + "nullable": true, + "type_": { + "primitive": null, + "array": { + "array": { + "primitive": "string" + } + }, + "enumeration": null + } + } + ], + "primary_key": [ + "film_id" + ] + } + } + "#, + ) +} diff --git a/src/builder.rs b/src/builder.rs index 48ff9321..eeda96d9 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,6 +18,8 @@ use crate::{ PaginationInputBuilder, }; +type MetadataHashMap = std::collections::HashMap; + /// The Builder is used to create the Schema for GraphQL /// /// You can populate it with the entities, enumerations of your choice @@ -52,7 +54,7 @@ pub struct Builder { pub subscriptions: Vec, /// holds all entities metadata - pub metadata: std::collections::HashMap, + pub metadata: MetadataHashMap, /// holds a copy to the database connection pub connection: sea_orm::DatabaseConnection, @@ -373,32 +375,36 @@ impl Builder { self } - /// used to consume the builder context and generate a ready to be completed GraphQL schema - pub fn schema_builder(self) -> SchemaBuilder { - let query = self.query; - let mutation = self.mutation; - let subscription = self.subscription; - let schema = self.schema; - let have_subscription = !self.subscriptions.is_empty(); + #[cfg(feature = "schema-meta")] + fn register_schema_meta(&mut self) { + use crate::schema; - // register queries - let query = self - .queries - .into_iter() - .fold(query, |query, field| query.field(field)); + self.register_custom_output::(); + self.register_custom_output::(); + self.register_custom_output::(); + self.register_custom_output::(); + self.register_custom_output::(); + + use crate::GqlOutputModelType; + use async_graphql::dynamic::FieldValue; const TABLE_NAME: &str = "table_name"; + let metadata_hashmap = std::mem::take(&mut self.metadata); + let field = Field::new( "_sea_orm_entity_metadata", - TypeRef::named(TypeRef::STRING), + crate::schema::Table::gql_output_type_ref(self.context), move |ctx| { - let metadata_hashmap = self.metadata.clone(); + let metadata_hashmap = metadata_hashmap.clone(); FieldFuture::new(async move { let table_name = ctx.args.try_get(TABLE_NAME)?.string()?; if let Some(metadata) = metadata_hashmap.get(table_name) { - Ok(Some(async_graphql::Value::from_json(metadata.to_owned())?)) + let table: crate::schema::Table = serde_json::from_value(metadata.clone())?; + Ok(Some(FieldValue::owned_any(table))) } else { - Ok(None) + Err(async_graphql::Error::new(format!( + "table not found `{table_name}`" + ))) } }) }, @@ -407,7 +413,26 @@ impl Builder { TABLE_NAME, TypeRef::named_nn(TypeRef::STRING), )); - let query = query.field(field); + + self.queries.push(field); + } + + /// used to consume the builder context and generate a ready to be completed GraphQL schema + pub fn schema_builder(mut self) -> SchemaBuilder { + #[cfg(feature = "schema-meta")] + self.register_schema_meta(); + + let query = self.query; + let mutation = self.mutation; + let subscription = self.subscription; + let schema = self.schema; + let have_subscription = !self.subscriptions.is_empty(); + + // register queries + let query = self + .queries + .into_iter() + .fold(query, |query, field| query.field(field)); // register mutations let mutation = self @@ -680,6 +705,13 @@ macro_rules! register_active_enums { }; } +#[macro_export] +macro_rules! register_custom_entities { + ($builder:expr, [$($entity:ident),+ $(,)?]) => { + $($builder.register_custom_entity::<$entity::Entity>();)* + }; +} + #[macro_export] macro_rules! register_custom_inputs { ($builder:expr, [$($ty:path),+ $(,)?]) => { diff --git a/src/lib.rs b/src/lib.rs index b9ba4389..da02afcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -300,6 +300,9 @@ pub use error::*; pub mod rbac; pub use rbac::*; +#[cfg(feature = "schema-meta")] +pub mod schema; + pub type SimpleNamingFn = Box String + Sync + Send>; pub type ComplexNamingFn = Box String + Sync + Send>; diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index 9bd66951..7fefed96 100644 --- a/src/outputs/entity_object.rs +++ b/src/outputs/entity_object.rs @@ -289,7 +289,7 @@ pub(crate) fn sea_query_value_to_graphql_value( #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] sea_orm::sea_query::Value::Json(value) => { - value.map(|it| match Value::from_json(*it.clone()) { + value.map(|it| match Value::from_json(it.clone()) { Ok(v) => v, Err(_) => Value::from(it.to_string()), }) diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index 7b64cb79..ca41691d 100644 --- a/src/query/entity_query_field.rs +++ b/src/query/entity_query_field.rs @@ -4,9 +4,9 @@ use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter}; use crate::{ apply_guard, apply_order, apply_pagination, get_filter_conditions, guard_error, - pluralize_unique, BuilderContext, ConnectionObjectBuilder, EntityColumnId, EntityObjectBuilder, - FilterInputBuilder, GuardAction, OperationType, OrderInputBuilder, PaginationInput, - PaginationInputBuilder,DatabaseContext,UserContext + pluralize_unique, BuilderContext, ConnectionObjectBuilder, DatabaseContext, EntityColumnId, + EntityObjectBuilder, FilterInputBuilder, GuardAction, OperationType, OrderInputBuilder, + PaginationInput, PaginationInputBuilder, UserContext, }; /// The configuration structure for EntityQueryFieldBuilder diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 00000000..81e4d32a --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,244 @@ +use crate as seaography; +use crate::macros::CustomOutput; +use serde::Deserialize; + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +pub struct Table { + pub columns: Vec, + pub primary_key: Vec, + pub comment: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +pub struct Column { + pub name: String, + #[serde(rename = "type")] + pub type_: ColumnType, + pub nullable: bool, + pub unique: Option, + pub comment: Option, +} + +#[derive(Debug, Clone, PartialEq, CustomOutput)] +pub struct ColumnType { + pub primitive: Option, + pub array: Option, + pub enumeration: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +pub struct Array { + pub array: Box, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +pub struct Enumeration { + pub name: String, + pub variants: Vec, // this requires `postgres-array` +} + +mod inner { + use super::*; + use serde::de::{self, Deserializer, MapAccess, Visitor}; + + impl<'de> Deserialize<'de> for ColumnType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(ColumnTypeVisitor) + } + } + + struct ColumnTypeVisitor; + + impl<'de> Visitor<'de> for ColumnTypeVisitor { + type Value = ColumnType; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a primitive string, an array object, or an enumeration object") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(ColumnType { + primitive: Some(value.to_string()), + array: None, + enumeration: None, + }) + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + // Deserialize into a temporary map to inspect keys + use serde_json::Value; + let mut value_map = serde_json::Map::new(); + + while let Some((key, value)) = map.next_entry::()? { + value_map.insert(key, value); + } + + let json_value = Value::Object(value_map); + + // Try to deserialize as Array + if let Ok(array) = Array::deserialize(json_value.clone()) { + return Ok(ColumnType { + primitive: None, + array: Some(array), + enumeration: None, + }); + } + + // Try to deserialize as Enumeration + if let Ok(enumeration) = Enumeration::deserialize(json_value.clone()) { + return Ok(ColumnType { + primitive: None, + array: None, + enumeration: Some(enumeration), + }); + } + + Err(de::Error::custom("Unknown ColumnType variant")) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_deser_schema() { + let table: Table = serde_json::from_str( + &r#"{ + "columns": [ + { + "name": "film_id", + "nullable": false, + "type": "integer" + }, + { + "name": "title", + "nullable": false, + "type": "string" + }, + { + "name": "last_update", + "nullable": false, + "type": "datetime" + }, + { + "name": "rating", + "nullable": true, + "type": { + "name": "mpaa_rating", + "variants": [ + "G", + "NC-17", + "PG", + "PG-13", + "R" + ] + } + }, + { + "name": "special_features", + "nullable": true, + "type": { + "array": "string" + } + } + ], + "primary_key": [ + "film_id" + ] + }"#, + ) + .unwrap(); + + assert_eq!( + table, + Table { + columns: vec![ + Column { + name: "film_id".into(), + nullable: false, + unique: None, + type_: ColumnType { + primitive: Some("integer".into()), + array: None, + enumeration: None + }, + comment: None, + }, + Column { + name: "title".into(), + nullable: false, + unique: None, + type_: ColumnType { + primitive: Some("string".into()), + array: None, + enumeration: None + }, + comment: None, + }, + Column { + name: "last_update".into(), + nullable: false, + unique: None, + type_: ColumnType { + primitive: Some("datetime".into()), + array: None, + enumeration: None + }, + comment: None, + }, + Column { + name: "rating".into(), + nullable: true, + unique: None, + type_: ColumnType { + primitive: None, + array: None, + enumeration: Some(Enumeration { + name: "mpaa_rating".into(), + variants: vec![ + "G".into(), + "NC-17".into(), + "PG".into(), + "PG-13".into(), + "R".into(), + ], + }) + }, + comment: None, + }, + Column { + name: "special_features".into(), + nullable: true, + unique: None, + type_: ColumnType { + primitive: None, + array: Some(Array { + array: ColumnType { + primitive: Some("string".into()), + array: None, + enumeration: None, + } + .into(), + }), + enumeration: None + }, + comment: None, + }, + ], + primary_key: vec!["film_id".into()], + comment: None, + } + ) + } +} From 910819658a667395a3de529abe7993f6885fded3 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 13 Sep 2025 20:54:01 +0100 Subject: [PATCH 04/85] Support #[seaography(ignore)] on Entity --- src/inputs/entity_input.rs | 4 ++++ src/inputs/filter_input.rs | 5 ++++- src/inputs/order_input.rs | 5 ++++- src/outputs/entity_object.rs | 4 ++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/inputs/entity_input.rs b/src/inputs/entity_input.rs index 770fef92..06c6d516 100644 --- a/src/inputs/entity_input.rs +++ b/src/inputs/entity_input.rs @@ -95,6 +95,10 @@ impl EntityInputBuilder { let column_def = column.def(); + if column_def.seaography().ignore { + return object; + } + let auto_increment = match ::from_column(column) { Some(_) => T::PrimaryKey::auto_increment(), None => false, diff --git a/src/inputs/filter_input.rs b/src/inputs/filter_input.rs index 7003b53c..0aa561a8 100644 --- a/src/inputs/filter_input.rs +++ b/src/inputs/filter_input.rs @@ -1,5 +1,5 @@ use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; -use sea_orm::{EntityTrait, Iterable}; +use sea_orm::{ColumnTrait, EntityTrait, Iterable}; use crate::{pluralize_unique, BuilderContext, EntityObjectBuilder, FilterTypesMapHelper}; @@ -48,6 +48,9 @@ impl FilterInputBuilder { let filter_name = self.type_name(&entity_name); let object = T::Column::iter().fold(InputObject::new(&filter_name), |object, column| { + if column.def().seaography().ignore { + return object; + } match filter_types_map_helper.get_column_filter_input_value::(&column) { Some(field) => object.field(field), None => object, diff --git a/src/inputs/order_input.rs b/src/inputs/order_input.rs index c1503e09..e3c437e7 100644 --- a/src/inputs/order_input.rs +++ b/src/inputs/order_input.rs @@ -1,5 +1,5 @@ use async_graphql::dynamic::{InputObject, InputValue, TypeRef, ValueAccessor}; -use sea_orm::{EntityTrait, Iterable}; +use sea_orm::{ColumnTrait, EntityTrait, Iterable}; use crate::{pluralize_unique, BuilderContext, EntityObjectBuilder, SeaResult, SeaographyError}; @@ -44,6 +44,9 @@ impl OrderInputBuilder { let name = self.type_name(&object_name); T::Column::iter().fold(InputObject::new(name), |object, column| { + if column.def().seaography().ignore { + return object; + } object.field(InputValue::new( entity_object_builder.column_name::(&column), TypeRef::named(&self.context.order_by_enum.type_name), diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index 7fefed96..89507ecb 100644 --- a/src/outputs/entity_object.rs +++ b/src/outputs/entity_object.rs @@ -130,6 +130,10 @@ impl EntityObjectBuilder { None => return object, }; + if column_def.seaography().ignore { + return object; + } + // This isn't the most beautiful flag: it's indicating whether the leaf type is an // enum, rather than the type itself. Ideally we'd only calculate this for the leaf // type itself. Could be a good candidate for refactor as this code evolves to support From cca44799770a0ea91785f3238c00f280f549330a Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 20 Sep 2025 14:31:42 +0100 Subject: [PATCH 05/85] Rebase on 1.1.x --- cli/Cargo.toml | 2 +- examples/mysql/Cargo.toml | 2 +- examples/postgres/Cargo.toml | 2 +- examples/postgres/tests/entity_metadata_tests.rs | 13 +++++++++++-- examples/postgres/tests/guard_tests.rs | 2 +- examples/sea-draw/Cargo.toml | 5 ++--- examples/sea-draw/src/lib.rs | 4 +--- examples/sea-draw/src/mutations.rs | 4 ++-- examples/sqlite/Cargo.toml | 2 +- macros/src/convert_output.rs | 2 +- src/builder.rs | 2 +- src/schema.rs | 12 ++++++------ 12 files changed, 29 insertions(+), 23 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7222e5e5..7885c0a9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } clap = { version = "4.3.19", features = ["derive"] } -seaography-generator = { version = "~2.0.0-rc.1", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index d7e1bc7a..6ce3d13b 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -14,7 +14,7 @@ tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.1" # seaography version +version = "~2.0.0-rc" # seaography version features = ["with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 9f593169..0b735bf4 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -14,7 +14,7 @@ tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.1" # seaography version +version = "~2.0.0-rc" # seaography version features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] diff --git a/examples/postgres/tests/entity_metadata_tests.rs b/examples/postgres/tests/entity_metadata_tests.rs index f6d9c433..40c22c40 100644 --- a/examples/postgres/tests/entity_metadata_tests.rs +++ b/examples/postgres/tests/entity_metadata_tests.rs @@ -2,7 +2,7 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; use seaography::async_graphql; -pub async fn get_schema() -> Schema { +pub async fn schema() -> Schema { let database = Database::connect("postgres://sea:sea@127.0.0.1/sakila") .await .unwrap(); @@ -20,7 +20,7 @@ pub fn assert_eq(a: Response, b: &str) { #[tokio::test] async fn test_entity_metadata() { - let schema = get_schema().await; + let schema = schema().await; assert_eq( schema @@ -244,6 +244,15 @@ async fn test_entity_metadata() { }, "enumeration": null } + }, + { + "name": "metadata", + "nullable": true, + "type_": { + "primitive": "json", + "array": null, + "enumeration": null + } } ], "primary_key": [ diff --git a/examples/postgres/tests/guard_tests.rs b/examples/postgres/tests/guard_tests.rs index afa4590c..66954070 100644 --- a/examples/postgres/tests/guard_tests.rs +++ b/examples/postgres/tests/guard_tests.rs @@ -280,7 +280,7 @@ async fn entity_guard() { .execute( r#" { - language(orderBy: { languageId: ASC }) { + language(filters: { languageId: { lte: 6 } }, orderBy: { languageId: ASC }) { nodes { languageId name diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index b39956ec..7468dd08 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } async-graphql-axum = { version = "7" } axum = { version = "0.8.*", features = ["ws"] } -chrono = { version = "0.4.30", default-features = false } clap = { version = "4.5", features = ["derive", "env"] } dotenv = "0.15.0" reqwest = { version = "0.12", features = [ @@ -16,7 +15,7 @@ reqwest = { version = "0.12", features = [ "rustls-tls", "multipart", ], default-features = false } -sea-orm = { version = "~1.1.14", features = [ +sea-orm = { version = "~2.0.0-rc", features = [ "sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", @@ -31,7 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } [dependencies.seaography] path = "../../" -version = "~1.1.4" # seaography version +version = "~2.0.0-rc" # seaography version features = [ "macros", "field-pluralize", diff --git a/examples/sea-draw/src/lib.rs b/examples/sea-draw/src/lib.rs index bdd6e787..298e4d55 100644 --- a/examples/sea-draw/src/lib.rs +++ b/examples/sea-draw/src/lib.rs @@ -14,9 +14,7 @@ pub mod subscriptions; pub mod types; pub fn never_condition() -> sea_orm::Condition { - sea_orm::query::Condition::any().add(sea_orm::sea_query::ConditionExpression::SimpleExpr( - sea_orm::sea_query::SimpleExpr::Constant(sea_orm::sea_query::Value::Bool(Some(false))), - )) + sea_orm::query::Condition::any().add(sea_orm::sea_query::Expr::val(false)) } pub fn permission_for_operation_type(op: seaography::OperationType) -> entities::Permission { diff --git a/examples/sea-draw/src/mutations.rs b/examples/sea-draw/src/mutations.rs index c291074c..d2aff780 100644 --- a/examples/sea-draw/src/mutations.rs +++ b/examples/sea-draw/src/mutations.rs @@ -4,10 +4,10 @@ use crate::{ types::{Fill, Shape, Stroke}, }; use async_graphql::Context; -use chrono::Utc; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, - entity::prelude::Uuid, sea_query::query::OnConflict, + entity::prelude::{ChronoUtc as Utc, Uuid}, + sea_query::query::OnConflict, }; use seaography::CustomFields; use tracing::instrument; diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 6d78ed83..8828d432 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -17,7 +17,7 @@ serde_json = { version = "1.0.103" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.1" # seaography version +version = "~2.0.0-rc" # seaography version features = ["macros", "with-decimal", "with-chrono"] [workspace] diff --git a/macros/src/convert_output.rs b/macros/src/convert_output.rs index b65f5037..b748c49f 100644 --- a/macros/src/convert_output.rs +++ b/macros/src/convert_output.rs @@ -77,7 +77,7 @@ fn derive_convert_output_enum_containers( ) -> async_graphql::Result>> { if let sea_orm::sea_query::value::Value::Json(opt_json) = value { if let Some(json) = opt_json { - match serde_json::from_value::<#orig_ident>(*json.clone()) { + match serde_json::from_value::<#orig_ident>(json.clone()) { Ok(obj) => match obj { #(#variant_matches)* }, diff --git a/src/builder.rs b/src/builder.rs index eeda96d9..48a9b6bc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -385,7 +385,7 @@ impl Builder { self.register_custom_output::(); self.register_custom_output::(); - use crate::GqlOutputModelType; + use crate::CustomOutputType; use async_graphql::dynamic::FieldValue; const TABLE_NAME: &str = "table_name"; diff --git a/src/schema.rs b/src/schema.rs index 81e4d32a..99c02869 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,15 +1,15 @@ use crate as seaography; -use crate::macros::CustomOutput; +use crate::CustomOutputType; use serde::Deserialize; -#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] pub struct Table { pub columns: Vec, pub primary_key: Vec, pub comment: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] pub struct Column { pub name: String, #[serde(rename = "type")] @@ -19,19 +19,19 @@ pub struct Column { pub comment: Option, } -#[derive(Debug, Clone, PartialEq, CustomOutput)] +#[derive(Debug, Clone, PartialEq, CustomOutputType)] pub struct ColumnType { pub primitive: Option, pub array: Option, pub enumeration: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] pub struct Array { pub array: Box, } -#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutput)] +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] pub struct Enumeration { pub name: String, pub variants: Vec, // this requires `postgres-array` From 6b1ff0ab4a0a7f07748ab4168377648ebbd05723 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 23 Sep 2025 23:35:53 +0100 Subject: [PATCH 06/85] Remove guards --- examples/mysql/tests/guard_mutation_tests.rs | 126 ------ examples/mysql/tests/guard_tests.rs | 381 ------------------ .../postgres/tests/guard_mutation_tests.rs | 126 ------ examples/postgres/tests/guard_tests.rs | 369 ----------------- examples/sea-draw/.gitignore | 1 + examples/sqlite/tests/guard_mutation_tests.rs | 124 ------ examples/sqlite/tests/guard_tests.rs | 137 ------- src/builder_context.rs | 1 - src/builder_context/guards.rs | 25 -- src/mutation/entity_create_batch_mutation.rs | 18 +- src/mutation/entity_create_one_mutation.rs | 17 +- src/mutation/entity_delete_mutation.rs | 10 +- src/mutation/entity_update_mutation.rs | 20 +- src/outputs/entity_object.rs | 13 +- src/query/entity_object_relation.rs | 15 +- src/query/entity_object_via_relation.rs | 11 +- src/query/entity_query_field.rs | 17 +- 17 files changed, 21 insertions(+), 1390 deletions(-) delete mode 100644 examples/mysql/tests/guard_mutation_tests.rs delete mode 100644 examples/mysql/tests/guard_tests.rs delete mode 100644 examples/postgres/tests/guard_mutation_tests.rs delete mode 100644 examples/postgres/tests/guard_tests.rs delete mode 100644 examples/sqlite/tests/guard_mutation_tests.rs delete mode 100644 examples/sqlite/tests/guard_tests.rs diff --git a/examples/mysql/tests/guard_mutation_tests.rs b/examples/mysql/tests/guard_mutation_tests.rs deleted file mode 100644 index 18e070d2..00000000 --- a/examples/mysql/tests/guard_mutation_tests.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::collections::BTreeMap; - -use async_graphql::{dynamic::*, Response}; -use sea_orm::Database; -use seaography::{async_graphql, lazy_static, BuilderContext, FnGuard, GuardsConfig}; - -lazy_static::lazy_static! { - static ref CONTEXT : BuilderContext = { - let context = BuilderContext::default(); - let mut entity_guards: BTreeMap = BTreeMap::new(); - entity_guards.insert("FilmCategory".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - let mut field_guards: BTreeMap = BTreeMap::new(); - field_guards.insert("Language.name".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - BuilderContext { - guards: GuardsConfig { - entity_guards, - field_guards, - }, - ..context - } - }; -} - -async fn schema() -> Schema { - let database = Database::connect("mysql://sea:sea@127.0.0.1/sakila") - .await - .unwrap(); - seaography_mysql_example::query_root::schema_builder(&CONTEXT, database, None, None) - .finish() - .unwrap() -} - -fn assert_eq(a: Response, b: &str) { - assert_eq!( - a.data.into_json().unwrap(), - serde_json::from_str::(b).unwrap() - ) -} - -#[tokio::test] -async fn entity_guard_mutation() { - let schema = schema().await; - - assert_eq( - schema - .execute( - r#" - mutation LanguageUpdate { - languageUpdate( - data: { lastUpdate: "2030-01-01 11:11:11 UTC" } - filter: { languageId: { eq: 6 } } - ) { - languageId - } - } - "#, - ) - .await, - r#" - { - "languageUpdate": [ - { - "languageId": 6 - } - ] - } - "#, - ); - - let response = schema - .execute( - r#" - mutation FilmCategoryUpdate { - filmCategoryUpdate( - data: { filmId: 1, categoryId: 1, lastUpdate: "2030-01-01 11:11:11 UTC" } - ) { - filmId - } - } -"#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); - - let response = schema - .execute( - r#" - mutation FilmCategoryDelete { - filmCategoryDelete(filter: { filmId: { eq: 2 } }) - } -"#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); -} - -#[tokio::test] -async fn field_guard_mutation() { - let schema = schema().await; - - let response = schema - .execute( - r#" - mutation LanguageUpdate { - languageUpdate(data: { name: "Cantonese" }, filter: { languageId: { eq: 6 } }) { - languageId - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Field guard triggered."); -} diff --git a/examples/mysql/tests/guard_tests.rs b/examples/mysql/tests/guard_tests.rs deleted file mode 100644 index 6f35aee2..00000000 --- a/examples/mysql/tests/guard_tests.rs +++ /dev/null @@ -1,381 +0,0 @@ -use std::collections::BTreeMap; - -use async_graphql::{dynamic::*, Response}; -use sea_orm::{Database, DatabaseConnection, RelationTrait}; -use seaography::{ - async_graphql, lazy_static, Builder, BuilderContext, EntityObjectRelationBuilder, - EntityObjectViaRelationBuilder, FnGuard, GuardsConfig, -}; - -lazy_static::lazy_static! { - static ref CONTEXT : BuilderContext = { - let context = BuilderContext::default(); - let mut entity_guards: BTreeMap = BTreeMap::new(); - entity_guards.insert("FilmCategory".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - let mut field_guards: BTreeMap = BTreeMap::new(); - field_guards.insert("Language.lastUpdate".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - BuilderContext { - guards: GuardsConfig { - entity_guards, - field_guards, - }, - ..context - } - }; -} - -fn schema( - database: DatabaseConnection, - depth: Option, - complexity: Option, -) -> Result { - let mut builder = Builder::new(&CONTEXT, database.clone()); - let entity_object_relation_builder = EntityObjectRelationBuilder { context: &CONTEXT }; - let entity_object_via_relation_builder = EntityObjectViaRelationBuilder { context: &CONTEXT }; - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "actor", - seaography_mysql_example::entities::film_actor::Relation::Actor.def(), - ), - entity_object_relation_builder - .get_relation::( - "film", - seaography_mysql_example::entities::film_actor::Relation::Film.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "customer", - seaography_mysql_example::entities::rental::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "inventory", - seaography_mysql_example::entities::rental::Relation::Inventory.def(), - ), - entity_object_relation_builder - .get_relation::( - "payment", - seaography_mysql_example::entities::rental::Relation::Payment.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_mysql_example::entities::rental::Relation::Staff.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_via_relation_builder - .get_relation::( - "film", - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_mysql_example::entities::staff::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "payment", - seaography_mysql_example::entities::staff::Relation::Payment.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_mysql_example::entities::staff::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "selfRef", - seaography_mysql_example::entities::staff::Relation::SelfRef.def(), - ), - entity_object_relation_builder - .get_relation::( - "selfRefReverse", - seaography_mysql_example::entities::staff::Relation::SelfRef.def().rev(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_mysql_example::entities::staff::Relation::Store.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "city", - seaography_mysql_example::entities::country::Relation::City.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_via_relation_builder - .get_relation::("actor"), - entity_object_via_relation_builder - .get_relation::( - "category", - ), - entity_object_relation_builder - .get_relation::( - "inventory", - seaography_mysql_example::entities::film::Relation::Inventory.def(), - ), - entity_object_relation_builder - .get_relation::( - "language1", - seaography_mysql_example::entities::film::Relation::Language1.def(), - ), - entity_object_relation_builder - .get_relation::( - "language2", - seaography_mysql_example::entities::film::Relation::Language2.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_via_relation_builder - .get_relation::("film"), - ]); - builder.register_entity::(vec![]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_mysql_example::entities::city::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "country", - seaography_mysql_example::entities::city::Relation::Country.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "film", - seaography_mysql_example::entities::inventory::Relation::Film.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_mysql_example::entities::inventory::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_mysql_example::entities::inventory::Relation::Store.def(), - ), - ]); - builder.register_entity::(vec![]); - builder . register_entity :: < seaography_mysql_example:: entities :: film_category :: Entity > (vec ! [entity_object_relation_builder . get_relation :: < seaography_mysql_example:: entities :: film_category :: Entity , seaography_mysql_example:: entities :: category :: Entity > ("category" , seaography_mysql_example:: entities :: film_category :: Relation :: Category . def ()) , entity_object_relation_builder . get_relation :: < seaography_mysql_example:: entities :: film_category :: Entity , seaography_mysql_example:: entities :: film :: Entity > ("film" , seaography_mysql_example:: entities :: film_category :: Relation :: Film . def ())]) ; - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_mysql_example::entities::customer::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "payment", - seaography_mysql_example::entities::customer::Relation::Payment.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_mysql_example::entities::customer::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_mysql_example::entities::customer::Relation::Store.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_mysql_example::entities::store::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "customer", - seaography_mysql_example::entities::store::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "inventory", - seaography_mysql_example::entities::store::Relation::Inventory.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_mysql_example::entities::store::Relation::Staff.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "customer", - seaography_mysql_example::entities::payment::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_mysql_example::entities::payment::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_mysql_example::entities::payment::Relation::Staff.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "city", - seaography_mysql_example::entities::address::Relation::City.def(), - ), - entity_object_relation_builder - .get_relation::( - "customer", - seaography_mysql_example::entities::address::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_mysql_example::entities::address::Relation::Staff.def(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_mysql_example::entities::address::Relation::Store.def(), - ), - ]); - builder - .register_enumeration::(); - builder - .set_depth_limit(depth) - .set_complexity_limit(complexity) - .schema_builder() - .data(database) - .finish() -} - -async fn get_schema() -> Schema { - let database = Database::connect("mysql://sea:sea@127.0.0.1/sakila") - .await - .unwrap(); - let schema = schema(database, None, None).unwrap(); - - schema -} - -fn assert_eq(a: Response, b: &str) { - assert_eq!( - a.data.into_json().unwrap(), - serde_json::from_str::(b).unwrap() - ) -} - -#[tokio::test] -async fn entity_guard() { - let schema = get_schema().await; - - assert_eq( - schema - .execute( - r#" - { - language { - nodes { - languageId - name - } - } - } - "#, - ) - .await, - r#" - { - "language": { - "nodes": [ - { - "languageId": 1, - "name": "English" - }, - { - "languageId": 2, - "name": "Italian" - }, - { - "languageId": 3, - "name": "Japanese" - }, - { - "languageId": 4, - "name": "Mandarin" - }, - { - "languageId": 5, - "name": "French" - }, - { - "languageId": 6, - "name": "German" - } - ] - } - } - "#, - ); - - let response = schema - .execute( - r#" - { - filmCategory { - nodes { - filmId - } - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); -} - -#[tokio::test] -async fn field_guard() { - let schema = get_schema().await; - - let response = schema - .execute( - r#" - { - language { - nodes { - languageId - name - lastUpdate - } - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Field guard triggered."); -} diff --git a/examples/postgres/tests/guard_mutation_tests.rs b/examples/postgres/tests/guard_mutation_tests.rs deleted file mode 100644 index 718853cb..00000000 --- a/examples/postgres/tests/guard_mutation_tests.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::collections::BTreeMap; - -use async_graphql::{dynamic::*, Response}; -use sea_orm::Database; -use seaography::{async_graphql, lazy_static, BuilderContext, FnGuard, GuardsConfig}; - -lazy_static::lazy_static! { - static ref CONTEXT : BuilderContext = { - let context = BuilderContext::default(); - let mut entity_guards: BTreeMap = BTreeMap::new(); - entity_guards.insert("FilmCategory".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - let mut field_guards: BTreeMap = BTreeMap::new(); - field_guards.insert("Language.name".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - BuilderContext { - guards: GuardsConfig { - entity_guards, - field_guards, - }, - ..context - } - }; -} - -async fn schema() -> Schema { - let database = Database::connect("postgres://sea:sea@127.0.0.1/sakila") - .await - .unwrap(); - seaography_postgres_example::query_root::schema_builder(&CONTEXT, database, None, None) - .finish() - .unwrap() -} - -fn assert_eq(a: Response, b: &str) { - assert_eq!( - a.data.into_json().unwrap(), - serde_json::from_str::(b).unwrap() - ) -} - -#[tokio::test] -async fn entity_guard_mutation() { - let schema = schema().await; - - assert_eq( - schema - .execute( - r#" - mutation LanguageUpdate { - languageUpdate( - data: { lastUpdate: "2030-01-01 11:11:11" } - filter: { languageId: { eq: 6 } } - ) { - languageId - } - } - "#, - ) - .await, - r#" - { - "languageUpdate": [ - { - "languageId": 6 - } - ] - } - "#, - ); - - let response = schema - .execute( - r#" - mutation FilmCategoryUpdate { - filmCategoryUpdate( - data: { filmId: 1, categoryId: 1, lastUpdate: "2030-01-01 11:11:11" } - ) { - filmId - } - } -"#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); - - let response = schema - .execute( - r#" - mutation FilmCategoryDelete { - filmCategoryDelete(filter: { filmId: { eq: 2 } }) - } -"#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); -} - -#[tokio::test] -async fn field_guard_mutation() { - let schema = schema().await; - - let response = schema - .execute( - r#" - mutation LanguageUpdate { - languageUpdate(data: { name: "Cantonese" }, filter: { languageId: { eq: 6 } }) { - languageId - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Field guard triggered."); -} diff --git a/examples/postgres/tests/guard_tests.rs b/examples/postgres/tests/guard_tests.rs deleted file mode 100644 index 66954070..00000000 --- a/examples/postgres/tests/guard_tests.rs +++ /dev/null @@ -1,369 +0,0 @@ -use std::collections::BTreeMap; - -use async_graphql::{dynamic::*, Response}; -use sea_orm::{Database, DatabaseConnection, RelationTrait}; -use seaography::{ - async_graphql, lazy_static, Builder, BuilderContext, EntityObjectRelationBuilder, - EntityObjectViaRelationBuilder, FnGuard, GuardsConfig, -}; - -lazy_static::lazy_static! { - static ref CONTEXT : BuilderContext = { - let context = BuilderContext::default(); - let mut entity_guards: BTreeMap = BTreeMap::new(); - entity_guards.insert("FilmCategory".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - let mut field_guards: BTreeMap = BTreeMap::new(); - field_guards.insert("Language.lastUpdate".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - BuilderContext { - guards: GuardsConfig { - entity_guards, - field_guards, - }, - ..context - } - }; -} - -pub fn schema( - database: DatabaseConnection, - depth: Option, - complexity: Option, -) -> Result { - let mut builder = Builder::new(&CONTEXT, database.clone()); - let entity_object_relation_builder = EntityObjectRelationBuilder { context: &CONTEXT }; - let entity_object_via_relation_builder = EntityObjectViaRelationBuilder { context: &CONTEXT }; - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "actor", - seaography_postgres_example::entities::film_actor::Relation::Actor.def(), - ), - entity_object_relation_builder - .get_relation::( - "film", - seaography_postgres_example::entities::film_actor::Relation::Film.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "customer", - seaography_postgres_example::entities::rental::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "inventory", - seaography_postgres_example::entities::rental::Relation::Inventory.def(), - ), - entity_object_relation_builder - .get_relation::( - "payment", - seaography_postgres_example::entities::rental::Relation::Payment.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_postgres_example::entities::rental::Relation::Staff.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_via_relation_builder - .get_relation::( - "film", - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_postgres_example::entities::staff::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "payment", - seaography_postgres_example::entities::staff::Relation::Payment.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_postgres_example::entities::staff::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_postgres_example::entities::staff::Relation::Store.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "city", - seaography_postgres_example::entities::country::Relation::City.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_via_relation_builder - .get_relation::("actor"), - entity_object_via_relation_builder - .get_relation::( - "category", - ), - entity_object_relation_builder - .get_relation::( - "inventory", - seaography_postgres_example::entities::film::Relation::Inventory.def(), - ), - entity_object_relation_builder - .get_relation::( - "language1", - seaography_postgres_example::entities::film::Relation::Language1.def(), - ), - entity_object_relation_builder - .get_relation::( - "language2", - seaography_postgres_example::entities::film::Relation::Language2.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_via_relation_builder - .get_relation::("film"), - ]); - builder.register_entity::(vec![]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_postgres_example::entities::city::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "country", - seaography_postgres_example::entities::city::Relation::Country.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "film", - seaography_postgres_example::entities::inventory::Relation::Film.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_postgres_example::entities::inventory::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_postgres_example::entities::inventory::Relation::Store.def(), - ), - ]); - builder . register_entity :: < seaography_postgres_example:: entities :: film_category :: Entity > (vec ! [entity_object_relation_builder . get_relation :: < seaography_postgres_example:: entities :: film_category :: Entity , seaography_postgres_example:: entities :: category :: Entity > ("category" , seaography_postgres_example:: entities :: film_category :: Relation :: Category . def ()) , entity_object_relation_builder . get_relation :: < seaography_postgres_example:: entities :: film_category :: Entity , seaography_postgres_example:: entities :: film :: Entity > ("film" , seaography_postgres_example:: entities :: film_category :: Relation :: Film . def ())]) ; - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_postgres_example::entities::customer::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "payment", - seaography_postgres_example::entities::customer::Relation::Payment.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_postgres_example::entities::customer::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_postgres_example::entities::customer::Relation::Store.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "address", - seaography_postgres_example::entities::store::Relation::Address.def(), - ), - entity_object_relation_builder - .get_relation::( - "customer", - seaography_postgres_example::entities::store::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "inventory", - seaography_postgres_example::entities::store::Relation::Inventory.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_postgres_example::entities::store::Relation::Staff.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "customer", - seaography_postgres_example::entities::payment::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "rental", - seaography_postgres_example::entities::payment::Relation::Rental.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_postgres_example::entities::payment::Relation::Staff.def(), - ), - ]); - builder.register_entity::(vec![ - entity_object_relation_builder - .get_relation::( - "city", - seaography_postgres_example::entities::address::Relation::City.def(), - ), - entity_object_relation_builder - .get_relation::( - "customer", - seaography_postgres_example::entities::address::Relation::Customer.def(), - ), - entity_object_relation_builder - .get_relation::( - "staff", - seaography_postgres_example::entities::address::Relation::Staff.def(), - ), - entity_object_relation_builder - .get_relation::( - "store", - seaography_postgres_example::entities::address::Relation::Store.def(), - ), - ]); - builder.register_enumeration::(); - builder - .set_depth_limit(depth) - .set_complexity_limit(complexity) - .schema_builder() - .data(database) - .finish() -} - -pub async fn get_schema() -> Schema { - let database = Database::connect("postgres://sea:sea@127.0.0.1/sakila") - .await - .unwrap(); - let schema = schema(database, None, None).unwrap(); - - schema -} - -pub fn assert_eq(a: Response, b: &str) { - assert_eq!( - a.data.into_json().unwrap(), - serde_json::from_str::(b).unwrap() - ) -} - -#[tokio::test] -async fn entity_guard() { - let schema = get_schema().await; - - assert_eq( - schema - .execute( - r#" - { - language(filters: { languageId: { lte: 6 } }, orderBy: { languageId: ASC }) { - nodes { - languageId - name - } - } - } - "#, - ) - .await, - r#" - { - "language": { - "nodes": [ - { - "languageId": 1, - "name": "English " - }, - { - "languageId": 2, - "name": "Italian " - }, - { - "languageId": 3, - "name": "Japanese " - }, - { - "languageId": 4, - "name": "Mandarin " - }, - { - "languageId": 5, - "name": "French " - }, - { - "languageId": 6, - "name": "German " - } - ] - } - } - "#, - ); - - let response = schema - .execute( - r#" - { - filmCategory { - nodes { - filmId - } - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); -} - -#[tokio::test] -async fn field_guard() { - let schema = get_schema().await; - - let response = schema - .execute( - r#" - { - language { - nodes { - languageId - name - lastUpdate - } - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Field guard triggered."); -} diff --git a/examples/sea-draw/.gitignore b/examples/sea-draw/.gitignore index 960b2c2c..8419ae98 100644 --- a/examples/sea-draw/.gitignore +++ b/examples/sea-draw/.gitignore @@ -1,2 +1,3 @@ sea_draw.db output*.svg +target/ \ No newline at end of file diff --git a/examples/sqlite/tests/guard_mutation_tests.rs b/examples/sqlite/tests/guard_mutation_tests.rs deleted file mode 100644 index b23b4917..00000000 --- a/examples/sqlite/tests/guard_mutation_tests.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::collections::BTreeMap; - -use async_graphql::{dynamic::*, Response}; -use sea_orm::Database; -use seaography::{async_graphql, lazy_static, BuilderContext, FnGuard, GuardsConfig}; - -lazy_static::lazy_static! { - static ref CONTEXT : BuilderContext = { - let context = BuilderContext::default(); - let mut entity_guards: BTreeMap = BTreeMap::new(); - entity_guards.insert("FilmCategory".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - let mut field_guards: BTreeMap = BTreeMap::new(); - field_guards.insert("Language.name".into(), Box::new(|_ctx| { - seaography::GuardAction::Block(None) - })); - BuilderContext { - guards: GuardsConfig { - entity_guards, - field_guards, - }, - ..context - } - }; -} - -async fn schema() -> Schema { - let database = Database::connect("sqlite://sakila.db").await.unwrap(); - seaography_sqlite_example::query_root::schema_builder(&CONTEXT, database, None, None) - .finish() - .unwrap() -} - -fn assert_eq(a: Response, b: &str) { - assert_eq!( - a.data.into_json().unwrap(), - serde_json::from_str::(b).unwrap() - ) -} - -#[tokio::test] -async fn entity_guard_mutation() { - let schema = schema().await; - - assert_eq( - schema - .execute( - r#" - mutation LanguageUpdate { - languageUpdate( - data: { lastUpdate: "2030-01-01 11:11:11 UTC" } - filter: { languageId: { eq: 6 } } - ) { - languageId - } - } - "#, - ) - .await, - r#" - { - "languageUpdate": [ - { - "languageId": 6 - } - ] - } - "#, - ); - - let response = schema - .execute( - r#" - mutation FilmCategoryUpdate { - filmCategoryUpdate( - data: { filmId: 1, categoryId: 1, lastUpdate: "2030-01-01 11:11:11 UTC" } - ) { - filmId - } - } -"#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); - - let response = schema - .execute( - r#" - mutation FilmCategoryDelete { - filmCategoryDelete(filter: { filmId: { eq: 2 } }) - } -"#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); -} - -#[tokio::test] -async fn field_guard_mutation() { - let schema = schema().await; - - let response = schema - .execute( - r#" - mutation LanguageUpdate { - languageUpdate(data: { name: "Cantonese" }, filter: { languageId: { eq: 6 } }) { - languageId - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Field guard triggered."); -} diff --git a/examples/sqlite/tests/guard_tests.rs b/examples/sqlite/tests/guard_tests.rs deleted file mode 100644 index 3488a91c..00000000 --- a/examples/sqlite/tests/guard_tests.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::collections::BTreeMap; - -use async_graphql::{dynamic::*, Response}; -use sea_orm::Database; -use seaography::{async_graphql, lazy_static, BuilderContext, FnGuard, GuardAction, GuardsConfig}; - -lazy_static::lazy_static! { - static ref CONTEXT : BuilderContext = { - let context = BuilderContext::default(); - let mut entity_guards: BTreeMap = BTreeMap::new(); - entity_guards.insert("FilmCategory".into(), Box::new(|_ctx| { - GuardAction::Block(None) - })); - let mut field_guards: BTreeMap = BTreeMap::new(); - field_guards.insert("Language.lastUpdate".into(), Box::new(|_ctx| { - GuardAction::Block(None) - })); - BuilderContext { - guards: GuardsConfig { - entity_guards, - field_guards, - }, - ..context - } - }; -} - -async fn schema() -> Schema { - let database = Database::connect("sqlite://sakila.db").await.unwrap(); - seaography_sqlite_example::query_root::schema_builder(&CONTEXT, database, None, None) - .finish() - .unwrap() -} - -fn assert_eq(a: Response, b: &str) { - assert_eq!( - a.data.into_json().unwrap(), - serde_json::from_str::(b).unwrap() - ) -} - -#[tokio::test] -async fn entity_guard() { - let schema = schema().await; - - assert_eq( - schema - .execute( - r#" - { - language { - nodes { - languageId - name - } - } - } - "#, - ) - .await, - r#" - { - "language": { - "nodes": [ - { - "languageId": 1, - "name": "English" - }, - { - "languageId": 2, - "name": "Italian" - }, - { - "languageId": 3, - "name": "Japanese" - }, - { - "languageId": 4, - "name": "Mandarin" - }, - { - "languageId": 5, - "name": "French" - }, - { - "languageId": 6, - "name": "German" - } - ] - } - } - "#, - ); - - let response = schema - .execute( - r#" - { - filmCategory { - nodes { - filmId - } - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Entity guard triggered."); -} - -#[tokio::test] -async fn field_guard() { - let schema = schema().await; - - let response = schema - .execute( - r#" - { - language { - nodes { - languageId - name - lastUpdate - } - } - } - "#, - ) - .await; - - assert_eq!(response.errors.len(), 1); - - assert_eq!(response.errors[0].message, "Field guard triggered."); -} diff --git a/src/builder_context.rs b/src/builder_context.rs index f82f5d6f..408bada0 100644 --- a/src/builder_context.rs +++ b/src/builder_context.rs @@ -55,7 +55,6 @@ pub struct BuilderContext { pub entity_input: EntityInputConfig, - pub guards: GuardsConfig, pub hooks: LifecycleHooks, pub types: TypesMapConfig, pub filter_types: FilterTypesMapConfig, diff --git a/src/builder_context/guards.rs b/src/builder_context/guards.rs index a28707b1..81aa6455 100644 --- a/src/builder_context/guards.rs +++ b/src/builder_context/guards.rs @@ -1,34 +1,9 @@ -use std::collections::BTreeMap; - -use async_graphql::dynamic::ResolverContext; - -/// Entities and Field guards configuration. -/// The guards are used to control access to entities or fields. -#[derive(Default)] -pub struct GuardsConfig { - /// entity guards are executed before accessing an entity - pub entity_guards: BTreeMap, - /// field guards are executed before accessing an entity field - pub field_guards: BTreeMap, -} - -/// guards are functions that receive the application context -pub type FnGuard = Box GuardAction + Sync + Send>; - #[derive(Debug, Clone, Eq, PartialEq)] pub enum GuardAction { Block(Option), Allow, } -pub fn apply_guard(ctx: &ResolverContext, guard: Option<&FnGuard>) -> GuardAction { - if let Some(guard) = guard { - (*guard)(ctx) - } else { - GuardAction::Allow - } -} - pub fn guard_error(reason: Option, fallback: &str) -> async_graphql::Error { match reason { Some(reason) => async_graphql::Error::new(reason), diff --git a/src/mutation/entity_create_batch_mutation.rs b/src/mutation/entity_create_batch_mutation.rs index d69a4011..4fc3cf65 100644 --- a/src/mutation/entity_create_batch_mutation.rs +++ b/src/mutation/entity_create_batch_mutation.rs @@ -4,9 +4,8 @@ use sea_orm::{ }; use crate::{ - apply_guard, guard_error, prepare_active_model, BuilderContext, DatabaseContext, - EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, - UserContext, + guard_error, prepare_active_model, BuilderContext, DatabaseContext, EntityInputBuilder, + EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityCreateBatchMutationBuilder @@ -72,8 +71,6 @@ impl EntityCreateBatchMutationBuilder { let context = self.context; let object_name: String = entity_object_builder.type_name::(); - let guard = self.context.guards.entity_guards.get(&object_name); - let field_guards = &self.context.guards.field_guards; let hooks = &self.context.hooks; Field::new( @@ -82,9 +79,6 @@ impl EntityCreateBatchMutationBuilder { move |ctx| { let object_name = object_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Create) { @@ -109,14 +103,6 @@ impl EntityCreateBatchMutationBuilder { { let input_object = &input.object()?; for (column, _) in input_object.iter() { - let field_guard = field_guards.get(&format!( - "{}.{}", - entity_object_builder.type_name::(), - column - )); - if let GuardAction::Block(reason) = apply_guard(&ctx, field_guard) { - return Err(guard_error(reason, "Field guard triggered.")); - } if let GuardAction::Block(reason) = hooks.field_guard(&ctx, &object_name, column, OperationType::Create) { diff --git a/src/mutation/entity_create_one_mutation.rs b/src/mutation/entity_create_one_mutation.rs index c17755cc..4f33d783 100644 --- a/src/mutation/entity_create_one_mutation.rs +++ b/src/mutation/entity_create_one_mutation.rs @@ -5,8 +5,8 @@ use sea_orm::{ }; use crate::{ - apply_guard, guard_error, BuilderContext, DatabaseContext, EntityInputBuilder, - EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, UserContext, + guard_error, BuilderContext, DatabaseContext, EntityInputBuilder, EntityObjectBuilder, + EntityQueryFieldBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityCreateOneMutationBuilder @@ -72,8 +72,6 @@ impl EntityCreateOneMutationBuilder { let context = self.context; let object_name: String = entity_object_builder.type_name::(); - let guard = self.context.guards.entity_guards.get(&object_name); - let field_guards = &self.context.guards.field_guards; let hooks = &self.context.hooks; Field::new( @@ -82,9 +80,6 @@ impl EntityCreateOneMutationBuilder { move |ctx| { let object_name = object_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Create) { @@ -99,14 +94,6 @@ impl EntityCreateOneMutationBuilder { let input_object = &value_accessor.object()?; for (column, _) in input_object.iter() { - let field_guard = field_guards.get(&format!( - "{}.{}", - entity_object_builder.type_name::(), - column - )); - if let GuardAction::Block(reason) = apply_guard(&ctx, field_guard) { - return Err(guard_error(reason, "Field guard triggered.")); - } if let GuardAction::Block(reason) = hooks.field_guard(&ctx, &object_name, column, OperationType::Create) { diff --git a/src/mutation/entity_delete_mutation.rs b/src/mutation/entity_delete_mutation.rs index 0e04fa57..9ff3ef62 100644 --- a/src/mutation/entity_delete_mutation.rs +++ b/src/mutation/entity_delete_mutation.rs @@ -4,9 +4,8 @@ use sea_orm::{ }; use crate::{ - apply_guard, get_filter_conditions, guard_error, BuilderContext, DatabaseContext, - EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, GuardAction, OperationType, - UserContext, + get_filter_conditions, guard_error, BuilderContext, DatabaseContext, EntityObjectBuilder, + EntityQueryFieldBuilder, FilterInputBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityDeleteMutationBuilder @@ -72,8 +71,6 @@ impl EntityDeleteMutationBuilder { let object_name_ = object_name.clone(); let context = self.context; - - let guard = self.context.guards.entity_guards.get(&object_name); let hooks = &self.context.hooks; Field::new( @@ -82,9 +79,6 @@ impl EntityDeleteMutationBuilder { move |ctx| { let object_name = object_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Delete) { diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs index fdbc33d4..173a68ba 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -5,9 +5,9 @@ use sea_orm::{ }; use crate::{ - apply_guard, get_filter_conditions, guard_error, prepare_active_model, BuilderContext, - DatabaseContext, EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, - FilterInputBuilder, GuardAction, OperationType, UserContext, + get_filter_conditions, guard_error, prepare_active_model, BuilderContext, DatabaseContext, + EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, + GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityUpdateMutationBuilder @@ -81,9 +81,6 @@ impl EntityUpdateMutationBuilder { let object_name_ = object_name.clone(); let context = self.context; - - let guard = self.context.guards.entity_guards.get(&object_name); - let field_guards = &self.context.guards.field_guards; let hooks = &self.context.hooks; Field::new( @@ -92,9 +89,6 @@ impl EntityUpdateMutationBuilder { move |ctx| { let object_name = object_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Update) { @@ -119,14 +113,6 @@ impl EntityUpdateMutationBuilder { let input_object = &value_accessor.object()?; for (column, _) in input_object.iter() { - let field_guard = field_guards.get(&format!( - "{}.{}", - entity_object_builder.type_name::(), - column - )); - if let GuardAction::Block(reason) = apply_guard(&ctx, field_guard) { - return Err(guard_error(reason, "Field guard triggered.")); - } if let GuardAction::Block(reason) = hooks.field_guard(&ctx, &object_name, column, OperationType::Update) { diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index 89507ecb..dca74872 100644 --- a/src/outputs/entity_object.rs +++ b/src/outputs/entity_object.rs @@ -8,7 +8,7 @@ use sea_orm::{ ModelTrait, TryIntoModel, }; -use crate::{apply_guard, guard_error, EntityColumnId, OperationType, SeaResult, SeaographyError}; +use crate::{guard_error, EntityColumnId, OperationType, SeaResult, SeaographyError}; /// The configuration structure for EntityObjectBuilder pub struct EntityObjectConfig { @@ -146,12 +146,6 @@ impl EntityObjectBuilder { _ => false, }; - let field_guard = self - .context - .guards - .field_guards - .get(&format!("{}.{}", &object_name, &column_name)); - let conversion_fn = self .context .types @@ -163,11 +157,6 @@ impl EntityObjectBuilder { let context = self.context; let field = Field::new(column_name.clone(), graphql_type, move |ctx| { - if let GuardAction::Block(reason) = apply_guard(&ctx, field_guard) { - return FieldFuture::new(async move { - Err::, _>(guard_error(reason, "Field guard triggered.")) - }); - } if let GuardAction::Block(reason) = hooks.field_guard(&ctx, &object_name, &column_name, OperationType::Read) { diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index 7040893a..52040a1e 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -8,10 +8,10 @@ use sea_orm::{ }; use crate::{ - apply_guard, apply_memory_pagination, get_filter_conditions, guard_error, pluralize_unique, - BuilderContext, Connection, ConnectionObjectBuilder, DatabaseContext, EntityObjectBuilder, - FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, - OperationType, OrderInputBuilder, PaginationInputBuilder, UserContext, + apply_memory_pagination, get_filter_conditions, guard_error, pluralize_unique, BuilderContext, + Connection, ConnectionObjectBuilder, DatabaseContext, EntityObjectBuilder, FilterInputBuilder, + GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, OperationType, + OrderInputBuilder, PaginationInputBuilder, UserContext, }; /// This builder produces a GraphQL field for an SeaORM entity relationship @@ -49,7 +49,6 @@ impl EntityObjectRelationBuilder { let parent_name: String = entity_object_builder.type_name::(); let object_name: String = entity_object_builder.type_name::(); let object_name_ = object_name.clone(); - let guard = self.context.guards.entity_guards.get(&object_name); let hooks = &self.context.hooks; let from_col = ::from_str( @@ -77,9 +76,6 @@ impl EntityObjectRelationBuilder { let parent_name = parent_name.clone(); let field_name = field_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) { @@ -145,9 +141,6 @@ impl EntityObjectRelationBuilder { let field_name = field_name.clone(); let context: &'static BuilderContext = context; FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) { diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 8754d26c..047a2620 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -9,8 +9,8 @@ use sea_orm::{ }; use crate::{ - apply_guard, apply_memory_pagination, apply_order, apply_pagination, get_filter_conditions, - guard_error, pluralize_unique, BuilderContext, ConnectionObjectBuilder, DatabaseContext, + apply_memory_pagination, apply_order, apply_pagination, get_filter_conditions, guard_error, + pluralize_unique, BuilderContext, ConnectionObjectBuilder, DatabaseContext, EntityObjectBuilder, FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, OperationType, OrderInputBuilder, PaginationInputBuilder, UserContext, @@ -60,7 +60,6 @@ impl EntityObjectViaRelationBuilder { let parent_name: String = entity_object_builder.type_name::(); let object_name: String = entity_object_builder.type_name::(); let object_name_ = object_name.clone(); - let guard = self.context.guards.entity_guards.get(&object_name); let hooks = &self.context.hooks; let from_col = ::from_str( @@ -88,9 +87,6 @@ impl EntityObjectViaRelationBuilder { let parent_name = parent_name.clone(); let field_name = field_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) { @@ -159,9 +155,6 @@ impl EntityObjectViaRelationBuilder { let parent_name = parent_name.clone(); let field_name = field_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) { diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index ca41691d..0feaa4d3 100644 --- a/src/query/entity_query_field.rs +++ b/src/query/entity_query_field.rs @@ -3,10 +3,10 @@ use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter}; use crate::{ - apply_guard, apply_order, apply_pagination, get_filter_conditions, guard_error, - pluralize_unique, BuilderContext, ConnectionObjectBuilder, DatabaseContext, EntityColumnId, - EntityObjectBuilder, FilterInputBuilder, GuardAction, OperationType, OrderInputBuilder, - PaginationInput, PaginationInputBuilder, UserContext, + apply_order, apply_pagination, get_filter_conditions, guard_error, pluralize_unique, + BuilderContext, ConnectionObjectBuilder, DatabaseContext, EntityColumnId, EntityObjectBuilder, + FilterInputBuilder, GuardAction, OperationType, OrderInputBuilder, PaginationInput, + PaginationInputBuilder, UserContext, }; /// The configuration structure for EntityQueryFieldBuilder @@ -97,8 +97,6 @@ impl EntityQueryFieldBuilder { }; let object_name = entity_object.type_name::(); - - let guard = self.context.guards.entity_guards.get(&object_name); let hooks = &self.context.hooks; let column = T::PrimaryKey::iter() @@ -122,9 +120,6 @@ impl EntityQueryFieldBuilder { move |ctx| { let object_name = object_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) { @@ -182,7 +177,6 @@ impl EntityQueryFieldBuilder { let object_name_ = object_name.clone(); let type_name = connection_object_builder.type_name(&object_name); - let guard = self.context.guards.entity_guards.get(&object_name); let hooks = &self.context.hooks; let context: &'static BuilderContext = self.context; let connection_name = pluralize_unique(&self.type_name_vanilla::(), true); @@ -190,9 +184,6 @@ impl EntityQueryFieldBuilder { Field::new(connection_name, TypeRef::named_nn(type_name), move |ctx| { let object_name = object_name.clone(); FieldFuture::new(async move { - if let GuardAction::Block(reason) = apply_guard(&ctx, guard) { - return Err(guard_error(reason, "Entity guard triggered.")); - } if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) { From 6ab59e4f468baa23c1525ca7d78f506e5fed197b Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 24 Sep 2025 00:42:36 +0100 Subject: [PATCH 07/85] enable combine_is_null_is_not_null by default --- examples/sea-draw/src/schema.rs | 1 - examples/sqlite/tests/more_query_tests.rs | 4 ---- examples/sqlite/tests/query_tests.rs | 2 +- src/query/entity_query_field.rs | 2 +- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/sea-draw/src/schema.rs b/examples/sea-draw/src/schema.rs index e34183e1..a6bad0a4 100644 --- a/examples/sea-draw/src/schema.rs +++ b/examples/sea-draw/src/schema.rs @@ -92,7 +92,6 @@ lazy_static::lazy_static! { basic_type_suffix: "Basic".into(), }, entity_query_field: EntityQueryFieldConfig { - combine_is_null_is_not_null: true, use_ilike: true, ..Default::default() }, diff --git a/examples/sqlite/tests/more_query_tests.rs b/examples/sqlite/tests/more_query_tests.rs index e874f002..7438197d 100644 --- a/examples/sqlite/tests/more_query_tests.rs +++ b/examples/sqlite/tests/more_query_tests.rs @@ -8,10 +8,6 @@ use seaography::{ lazy_static::lazy_static! { static ref CONTEXT : BuilderContext = { BuilderContext { - entity_query_field: EntityQueryFieldConfig { - combine_is_null_is_not_null: true, - ..Default::default() - }, pagination_input: PaginationInputConfig{ default_limit: Some(3), max_limit: Some(10), diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index 94d3b9a4..4473cad8 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -1143,7 +1143,7 @@ async fn filter_is_null() { r#" { address( - filters: { address: { contains: "Lane" }, postalCode: { is_not_null: "" } } + filters: { address: { contains: "Lane" }, postalCode: { is_null: false } } pagination: { page: { page: 0, limit: 2 } } ) { nodes { diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index 0feaa4d3..21c6d309 100644 --- a/src/query/entity_query_field.rs +++ b/src/query/entity_query_field.rs @@ -47,7 +47,7 @@ impl std::default::Default for EntityQueryFieldConfig { .into() }, pagination: "pagination".into(), - combine_is_null_is_not_null: false, + combine_is_null_is_not_null: true, use_ilike: false, } } From 1ce0aca7ddb869435ddae3c5bd545c81be14d5c4 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 24 Sep 2025 12:21:33 +0100 Subject: [PATCH 08/85] Stop patching --- .github/workflows/tests.yaml | 30 +++++++++++++----------------- Cargo.toml | 4 ++-- src/builder_context/types_map.rs | 4 +++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4171986d..0289223f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,9 +38,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: rustfmt, clippy - run: cargo fmt --all -- --check - working-directory: ./examples/sea-draw @@ -52,6 +51,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - name: Install sea-orm-cli uses: baptiste0928/cargo-install@v3 with: @@ -113,6 +114,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - name: Install sea-orm-cli uses: baptiste0928/cargo-install@v3 with: @@ -174,6 +177,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - name: Install sea-orm-cli uses: baptiste0928/cargo-install@v3 with: @@ -224,20 +229,8 @@ jobs: - name: Integration tests working-directory: ./examples/postgres run: cargo test - integration-sqlite-sea-draw: - name: SQLite integration tests (sea-draw) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - name: Build sea-draw - working-directory: ./examples/sea-draw - run: cargo build - - name: Run sea-draw integration tests - working-directory: ./examples/sea-draw - run: ./integration_test_sqlite.sh - integration-postgres-sea-draw: - name: Postgres integration tests (sea-draw) + integration-sea-draw: + name: sea-draw Integration tests runs-on: ubuntu-latest services: postgres: @@ -257,6 +250,9 @@ jobs: - name: Build sea-draw working-directory: ./examples/sea-draw run: cargo build - - name: Run sea-draw integration tests + - name: Run SQLite integration tests + working-directory: ./examples/sea-draw + run: ./integration_test_sqlite.sh + - name: Run Postgres integration tests working-directory: ./examples/sea-draw run: ./integration_test_postgres.sh diff --git a/Cargo.toml b/Cargo.toml index 173b1445..0b6eaf89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,5 +51,5 @@ field-camel-case = [] field-pluralize = ["pluralizer"] strict-custom-types = ["seaography-macros/strict-custom-types"] -[patch.crates-io] -sea-orm = { git = "https://github.com/SeaQL/sea-orm", branch = "master" } +# [patch.crates-io] +# sea-orm = { git = "https://github.com/SeaQL/sea-orm", branch = "master" } diff --git a/src/builder_context/types_map.rs b/src/builder_context/types_map.rs index e6111437..873b1fea 100644 --- a/src/builder_context/types_map.rs +++ b/src/builder_context/types_map.rs @@ -578,7 +578,9 @@ pub fn converted_type_to_sea_orm_array_type( } } -#[allow(unused_variables)] // some conversions behind feature flags need extra params here. +// usage behind feature flags +#[allow(clippy::only_used_in_recursion)] +#[allow(unused_variables)] pub fn converted_value_to_sea_orm_value( column_type: &ConvertedType, value: &ValueAccessor, From e47a07aa356dab3032de0bcc97d1f6f04b113202 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 27 Sep 2025 22:13:04 +0100 Subject: [PATCH 09/85] Filter by related entity --- examples/sqlite/tests/query_tests.rs | 131 +++++++++++++++++++ src/builder.rs | 58 ++++++--- src/builder_context.rs | 7 +- src/inputs/having_input.rs | 60 +++++++++ src/inputs/mod.rs | 3 + src/query/entity_object_relation.rs | 26 ++-- src/query/entity_object_via_relation.rs | 38 +++--- src/query/entity_query_field.rs | 20 ++- src/query/filtering.rs | 8 +- src/query/having.rs | 161 ++++++++++++++++++++++++ src/query/mod.rs | 3 + 11 files changed, 469 insertions(+), 46 deletions(-) create mode 100644 src/inputs/having_input.rs create mode 100644 src/query/having.rs diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index 4473cad8..da9f35d6 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -1179,3 +1179,134 @@ async fn filter_is_null() { "#, ); } + +#[tokio::test] +async fn film_having_actor() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + film( + having: { actor: { firstName: { eq: "BOB" } } } + orderBy: { filmId: ASC } + pagination: { page: { page: 0, limit: 2 } } + ) { + nodes { + filmId + title + actor { + nodes { + firstName + lastName + } + } + } + } + } + "#, + ) + .await, + r#" + { + "film": { + "nodes": [ + { + "filmId": 2, + "title": "ACE GOLDFINGER", + "actor": { + "nodes": [ + { + "firstName": "BOB", + "lastName": "FAWCETT" + }, + { + "firstName": "MINNIE", + "lastName": "ZELLWEGER" + }, + { + "firstName": "SEAN", + "lastName": "GUINESS" + }, + { + "firstName": "CHRIS", + "lastName": "DEPP" + } + ] + } + }, + { + "filmId": 3, + "title": "ADAPTATION HOLES", + "actor": { + "nodes": [ + { + "firstName": "NICK", + "lastName": "WAHLBERG" + }, + { + "firstName": "BOB", + "lastName": "FAWCETT" + }, + { + "firstName": "CAMERON", + "lastName": "STREEP" + }, + { + "firstName": "RAY", + "lastName": "JOHANSSON" + }, + { + "firstName": "JULIANNE", + "lastName": "DENCH" + } + ] + } + } + ] + } + } + "#, + ) +} + +#[tokio::test] +async fn country_having_city() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + country( + having: { city: { city: { eq: "London" } } } + orderBy: { countryId: ASC } + pagination: { page: { page: 0, limit: 10 } } + ) { + nodes { + country + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "Canada" + }, + { + "country": "United Kingdom" + } + ] + } + } + "#, + ) +} diff --git a/src/builder.rs b/src/builder.rs index 48a9b6bc..91a896dc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -13,9 +13,10 @@ use crate::{ CustomUnion, EdgeObjectBuilder, EntityCreateBatchMutationBuilder, EntityCreateOneMutationBuilder, EntityDeleteMutationBuilder, EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, EntityUpdateMutationBuilder, FilterInputBuilder, - FilterTypesMapHelper, OffsetInputBuilder, OneToManyLoader, OneToOneLoader, OrderByEnumBuilder, - OrderInputBuilder, PageInfoObjectBuilder, PageInputBuilder, PaginationInfoObjectBuilder, - PaginationInputBuilder, + FilterTypesMapHelper, HavingInputBuilder, OffsetInputBuilder, OneToManyLoader, OneToOneLoader, + OrderByEnumBuilder, OrderInputBuilder, PageInfoObjectBuilder, PageInputBuilder, + PaginationInfoObjectBuilder, PaginationInputBuilder, RelatedEntityFilter, + RelatedEntityFilterField, }; type MetadataHashMap = std::collections::HashMap; @@ -108,8 +109,11 @@ impl Builder { } /// used to register a new entity to the Builder context - pub fn register_entity(&mut self, relations: Vec) - where + pub fn register_entity( + &mut self, + relations: Vec, + related_entity_filter: &RelatedEntityFilter, + ) where T: EntityTrait, ::Model: Sync, { @@ -131,18 +135,24 @@ impl Builder { }; let connection = connection_object_builder.to_object::(); - self.outputs.extend(vec![entity_object, edge, connection]); + self.outputs.extend([entity_object, edge, connection]); let filter_input_builder = FilterInputBuilder { context: self.context, }; let filter = filter_input_builder.to_object::(); + let having_input_builder = HavingInputBuilder { + context: self.context, + }; + let having = having_input_builder.to_object::(related_entity_filter); + let order_input_builder = OrderInputBuilder { context: self.context, }; let order = order_input_builder.to_object::(); - self.inputs.extend(vec![filter, order]); + + self.inputs.extend([filter, having, order]); let entity_query_field_builder = EntityQueryFieldBuilder { context: self.context, @@ -208,7 +218,7 @@ impl Builder { let entity_insert_input_object = entity_input_builder.insert_input_object::(); let entity_update_input_object = entity_input_builder.update_input_object::(); self.inputs - .extend(vec![entity_insert_input_object, entity_update_input_object]); + .extend([entity_insert_input_object, entity_update_input_object]); // create one mutation let entity_create_one_mutation_builder = EntityCreateOneMutationBuilder { @@ -271,6 +281,17 @@ impl Builder { self } + pub fn register_related_entity_filter( + mut self, + related_entity_filter: RelatedEntityFilter, + ) -> Self + where + T: EntityTrait, + { + self.schema = self.schema.data(related_entity_filter); + self + } + /// used to register a new enumeration to the builder context pub fn register_enumeration(&mut self) where @@ -562,10 +583,14 @@ impl Builder { } pub trait RelationBuilder { - fn get_relation( + fn get_relation_name(&self, context: &'static BuilderContext) -> String; + + fn get_relation(&self, context: &'static BuilderContext) -> async_graphql::dynamic::Field; + + fn get_related_entity_filter( &self, - context: &'static crate::BuilderContext, - ) -> async_graphql::dynamic::Field; + context: &'static BuilderContext, + ) -> RelatedEntityFilterField; } #[macro_export] @@ -638,15 +663,20 @@ macro_rules! impl_custom_output_type_for_entity { #[macro_export] macro_rules! register_entity { ($builder:expr, $module_path:ident) => { - $builder.register_entity::<$module_path::Entity>( + let relations = <$module_path::RelatedEntity as sea_orm::Iterable>::iter() .map(|rel| seaography::RelationBuilder::get_relation(&rel, $builder.context)) - .collect(), - ); + .collect(); + let related_entity_filter = + seaography::RelatedEntityFilter::<$module_path::Entity>::build::<$module_path::RelatedEntity>($builder.context); + + $builder.register_entity::<$module_path::Entity>(relations, &related_entity_filter); $builder = $builder.register_entity_dataloader_one_to_one($module_path::Entity, tokio::spawn); $builder = $builder.register_entity_dataloader_one_to_many($module_path::Entity, tokio::spawn); + $builder = + $builder.register_related_entity_filter::<$module_path::Entity>(related_entity_filter); $builder.register_entity_mutations::<$module_path::Entity, $module_path::ActiveModel>(); }; } diff --git a/src/builder_context.rs b/src/builder_context.rs index 408bada0..6b969fcb 100644 --- a/src/builder_context.rs +++ b/src/builder_context.rs @@ -2,9 +2,9 @@ use crate::{ ActiveEnumConfig, ActiveEnumFilterInputConfig, ConnectionObjectConfig, CursorInputConfig, EdgeObjectConfig, EntityCreateBatchMutationConfig, EntityCreateOneMutationConfig, EntityDeleteMutationConfig, EntityInputConfig, EntityObjectConfig, EntityQueryFieldConfig, - EntityUpdateMutationConfig, FilterInputConfig, OffsetInputConfig, OrderByEnumConfig, - OrderInputConfig, PageInfoObjectConfig, PageInputConfig, PaginationInfoObjectConfig, - PaginationInputConfig, + EntityUpdateMutationConfig, FilterInputConfig, HavingInputConfig, OffsetInputConfig, + OrderByEnumConfig, OrderInputConfig, PageInfoObjectConfig, PageInputConfig, + PaginationInfoObjectConfig, PaginationInputConfig, }; pub mod entity_column_id; @@ -39,6 +39,7 @@ pub struct BuilderContext { pub order_input: OrderInputConfig, pub filter_input: FilterInputConfig, + pub having_input: HavingInputConfig, pub active_enum_filter_input: ActiveEnumFilterInputConfig, pub page_info_object: PageInfoObjectConfig, diff --git a/src/inputs/having_input.rs b/src/inputs/having_input.rs new file mode 100644 index 00000000..045ce56e --- /dev/null +++ b/src/inputs/having_input.rs @@ -0,0 +1,60 @@ +use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; +use sea_orm::EntityTrait; + +use crate::{pluralize_unique, BuilderContext, EntityObjectBuilder, RelatedEntityFilter}; + +/// The configuration structure for HavingInputBuilder +pub struct HavingInputConfig { + /// the filter input type name formatter function + pub type_name: crate::SimpleNamingFn, +} + +impl std::default::Default for HavingInputConfig { + fn default() -> Self { + HavingInputConfig { + type_name: Box::new(|object_name: &str| -> String { + format!("{object_name}HavingInput") + }), + } + } +} + +/// This builder is used to produce the filter input object of a SeaORM entity +pub struct HavingInputBuilder { + pub context: &'static BuilderContext, +} + +impl HavingInputBuilder { + /// used to get the filter input object name + /// object_name is the name of the SeaORM Entity GraphQL object + pub fn type_name(&self, object_name: &str) -> String { + let object_name = pluralize_unique(object_name, false); + self.context.having_input.type_name.as_ref()(&object_name) + } + + /// used to produce the filter input object of a SeaORM entity + pub fn to_object(&self, related_entity_filter: &RelatedEntityFilter) -> InputObject + where + T: EntityTrait, + { + let entity_object_builder = EntityObjectBuilder { + context: self.context, + }; + let entity_name = entity_object_builder.type_name::(); + let field_name = self.type_name(&entity_name); + + let related_fields = related_entity_filter.field_names(); + + let mut object = InputObject::new(&field_name); + + if related_fields.is_empty() { + object = object.field(InputValue::new("_", TypeRef::named(TypeRef::BOOLEAN))); + } + + for (field_name, filter_input) in related_fields { + object = object.field(InputValue::new(field_name, TypeRef::named(filter_input))); + } + + object + } +} diff --git a/src/inputs/mod.rs b/src/inputs/mod.rs index 54dd50d1..5997beb7 100644 --- a/src/inputs/mod.rs +++ b/src/inputs/mod.rs @@ -20,6 +20,9 @@ pub use order_input::*; pub mod filter_input; pub use filter_input::*; +pub mod having_input; +pub use having_input::*; + pub mod entity_input; pub use entity_input::*; diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index 52040a1e..be401e2c 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -21,25 +21,37 @@ pub struct EntityObjectRelationBuilder { } impl EntityObjectRelationBuilder { - /// used to get a GraphQL field for an SeaORM entity relationship - pub fn get_relation(&self, name: &str, relation_definition: RelationDef) -> Field + /// to be called by SeaORM + pub fn get_relation_name(&self, name: &str, relation_definition: RelationDef) -> String where T: EntityTrait, - <::Column as std::str::FromStr>::Err: core::fmt::Debug, R: EntityTrait, - ::Model: Sync, - <::Column as std::str::FromStr>::Err: core::fmt::Debug, { + self.get_relation_name_for(name, &relation_definition) + } + + fn get_relation_name_for(&self, name: &str, relation_definition: &RelationDef) -> String { let name_pp = &if cfg!(feature = "field-snake-case") { name.to_snake_case() } else { name.to_lower_camel_case() }; - let name = pluralize_unique( + pluralize_unique( name_pp, matches!(relation_definition.rel_type, sea_orm::RelationType::HasMany), - ); + ) + } + /// used to get a GraphQL field for an SeaORM entity relationship + pub fn get_relation(&self, name: &str, relation_definition: RelationDef) -> Field + where + T: EntityTrait, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + R: EntityTrait, + ::Model: Sync, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + { + let name = self.get_relation_name_for(name, &relation_definition); let context: &'static BuilderContext = self.context; let entity_object_builder = EntityObjectBuilder { context }; let connection_object_builder = ConnectionObjectBuilder { context }; diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 047a2620..68527477 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -5,7 +5,7 @@ use async_graphql::{ use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, - QueryTrait, Related, + QueryTrait, Related, RelationDef, }; use crate::{ @@ -23,29 +23,39 @@ pub struct EntityObjectViaRelationBuilder { } impl EntityObjectViaRelationBuilder { - /// used to get a GraphQL field for an SeaORM entity related trait - pub fn get_relation(&self, name: &str) -> Field + /// to be called by SeaORM + pub fn get_relation_name(&self, name: &str) -> String where - T: Related, - T: EntityTrait, + T: EntityTrait + Related, R: EntityTrait, - ::Model: Sync, - <::Column as std::str::FromStr>::Err: core::fmt::Debug, - <::Column as std::str::FromStr>::Err: core::fmt::Debug, { let to_relation_definition = >::to(); + self.get_relation_name_for(name, &to_relation_definition) + } + + fn get_relation_name_for(&self, name: &str, relation_definition: &RelationDef) -> String { let name_pp = if cfg!(feature = "field-snake-case") { name.to_snake_case() } else { name.to_lower_camel_case() }; - let name = pluralize_unique( + pluralize_unique( &name_pp, - matches!( - to_relation_definition.rel_type, - sea_orm::RelationType::HasMany - ), - ); + matches!(relation_definition.rel_type, sea_orm::RelationType::HasMany), + ) + } + + /// used to get a GraphQL field for an SeaORM entity related trait + pub fn get_relation(&self, name: &str) -> Field + where + T: EntityTrait + Related, + R: EntityTrait, + ::Model: Sync, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + { + let to_relation_definition = >::to(); + let name = self.get_relation_name_for(name, &to_relation_definition); let context: &'static BuilderContext = self.context; let (via_relation_definition, is_via_relation) = match >::via() { Some(def) => (def, true), diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index 21c6d309..c550d953 100644 --- a/src/query/entity_query_field.rs +++ b/src/query/entity_query_field.rs @@ -3,10 +3,10 @@ use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter}; use crate::{ - apply_order, apply_pagination, get_filter_conditions, guard_error, pluralize_unique, - BuilderContext, ConnectionObjectBuilder, DatabaseContext, EntityColumnId, EntityObjectBuilder, - FilterInputBuilder, GuardAction, OperationType, OrderInputBuilder, PaginationInput, - PaginationInputBuilder, UserContext, + apply_order, apply_pagination, get_filter_conditions, get_having_conditions, guard_error, + pluralize_unique, BuilderContext, ConnectionObjectBuilder, DatabaseContext, EntityColumnId, + EntityObjectBuilder, FilterInputBuilder, GuardAction, HavingInputBuilder, OperationType, + OrderInputBuilder, PaginationInput, PaginationInputBuilder, UserContext, }; /// The configuration structure for EntityQueryFieldBuilder @@ -15,6 +15,8 @@ pub struct EntityQueryFieldConfig { pub type_name: crate::SimpleNamingFn, /// name for 'filters' field pub filters: String, + /// name for 'having' field + pub having: String, /// name for 'orderBy' field pub order_by: String, /// name for 'pagination' field @@ -38,6 +40,7 @@ impl std::default::Default for EntityQueryFieldConfig { } }), filters: "filters".into(), + having: "having".into(), order_by: { if cfg!(feature = "field-snake-case") { "order_by" @@ -163,6 +166,9 @@ impl EntityQueryFieldBuilder { let filter_input_builder = FilterInputBuilder { context: self.context, }; + let having_input_builder = HavingInputBuilder { + context: self.context, + }; let order_input_builder = OrderInputBuilder { context: self.context, }; @@ -192,6 +198,8 @@ impl EntityQueryFieldBuilder { let filters = ctx.args.get(&context.entity_query_field.filters); let filters = get_filter_conditions::(context, filters)?; + let having = ctx.args.get(&context.entity_query_field.having); + let filters = get_having_conditions::(context, &ctx, filters, having)?; let order_by = ctx.args.get(&context.entity_query_field.order_by); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; let pagination = ctx.args.get(&context.entity_query_field.pagination); @@ -218,6 +226,10 @@ impl EntityQueryFieldBuilder { &self.context.entity_query_field.filters, TypeRef::named(filter_input_builder.type_name(&object_name_)), )) + .argument(InputValue::new( + &self.context.entity_query_field.having, + TypeRef::named(having_input_builder.type_name(&object_name_)), + )) .argument(InputValue::new( &self.context.entity_query_field.order_by, TypeRef::named(order_input_builder.type_name(&object_name_)), diff --git a/src/query/filtering.rs b/src/query/filtering.rs index 8199f697..1393b7b8 100644 --- a/src/query/filtering.rs +++ b/src/query/filtering.rs @@ -15,7 +15,7 @@ where if let Some(filters) = filters { let filters = filters.object()?; - recursive_prepare_condition::(context, filters) + recursive_prepare_condition::(context, &filters) } else { Ok(Condition::all()) } @@ -24,7 +24,7 @@ where /// used to prepare recursively the query filtering condition pub fn recursive_prepare_condition( context: &'static BuilderContext, - filters: ObjectAccessor, + filters: &ObjectAccessor, ) -> SeaResult where T: EntityTrait, @@ -56,7 +56,7 @@ where Condition::all(), |condition, filters: ValueAccessor| -> SeaResult { let filters = filters.object()?; - Ok(condition.add(recursive_prepare_condition::(context, filters)?)) + Ok(condition.add(recursive_prepare_condition::(context, &filters)?)) }, )?; @@ -72,7 +72,7 @@ where Condition::any(), |condition, filters: ValueAccessor| -> SeaResult { let filters = filters.object()?; - Ok(condition.add(recursive_prepare_condition::(context, filters)?)) + Ok(condition.add(recursive_prepare_condition::(context, &filters)?)) }, )?; diff --git a/src/query/having.rs b/src/query/having.rs new file mode 100644 index 00000000..04807368 --- /dev/null +++ b/src/query/having.rs @@ -0,0 +1,161 @@ +use async_graphql::dynamic::{ObjectAccessor, ResolverContext, ValueAccessor}; +use sea_orm::{ + sea_query::Expr, Condition, EntityTrait, Iterable, QueryFilter, QuerySelect, QueryTrait, + Related, RelationDef, +}; +use std::marker::PhantomData; + +use crate::{ + recursive_prepare_condition, BuilderContext, EntityObjectBuilder, FilterInputBuilder, + RelationBuilder, SeaResult, +}; + +/// utility function used to create the query filter condition +/// for a SeaORM entity using query filter inputs of related entities +pub fn get_having_conditions( + context: &'static BuilderContext, + ctx: &ResolverContext, + condition: Condition, + having: Option, +) -> SeaResult +where + T: EntityTrait, +{ + if let Some(having) = having { + let having = having.object()?; + let related = ctx.data_unchecked::>(); + related.apply(context, condition, &having) + } else { + Ok(condition) + } +} + +pub struct RelatedEntityFilterBuilder { + pub context: &'static BuilderContext, +} + +type FnFilterCondition = + Box SeaResult> + Send + Sync>; + +pub struct RelatedEntityFilter +where + E: EntityTrait, +{ + fields: Vec, + entity: PhantomData, +} + +pub struct RelatedEntityFilterField { + name: String, + filter_input: String, + filter_condition_fn: FnFilterCondition, +} + +impl RelatedEntityFilter +where + E: EntityTrait, +{ + pub fn build(context: &'static BuilderContext) -> Self + where + T: Iterable + RelationBuilder, + { + Self { + fields: T::iter() + .map(|rel| rel.get_related_entity_filter(context)) + .collect(), + entity: PhantomData, + } + } + + /// (field_name, filter_input) + pub fn field_names(&self) -> Vec<(String, String)> { + self.fields + .iter() + .map(|f| (f.name.clone(), f.filter_input.clone())) + .collect() + } + + fn apply( + &self, + context: &'static BuilderContext, + mut condition: Condition, + having: &ObjectAccessor, + ) -> SeaResult { + for field in &self.fields { + if let Some(filter) = having.get(&field.name) { + let filter = filter.object()?; + if let Some(additional) = (field.filter_condition_fn)(context, &filter)? { + condition = condition.add(additional); + } + } + } + Ok(condition) + } +} + +impl RelatedEntityFilterBuilder { + pub fn get_relation_via(&self, name: &str) -> RelatedEntityFilterField + where + T: EntityTrait + Related, + R: EntityTrait, + { + RelatedEntityFilterField::new::( + self.context, + name.to_owned(), + >::to(), + >::via(), + ) + } + + pub fn get_relation(&self, name: &str, to: RelationDef) -> RelatedEntityFilterField + where + T: EntityTrait, + R: EntityTrait, + { + RelatedEntityFilterField::new::(self.context, name.to_owned(), to, None) + } +} + +impl RelatedEntityFilterField { + fn new( + context: &'static BuilderContext, + name: String, + to: RelationDef, + via: Option, + ) -> Self + where + R: EntityTrait, + { + Self { + name, + filter_input: { + let entity_object_builder = EntityObjectBuilder { context }; + let filter_input_builder = FilterInputBuilder { context }; + let object_name: String = entity_object_builder.type_name::(); + filter_input_builder.type_name(&object_name) + }, + filter_condition_fn: Box::new(move |context, filter| -> SeaResult> { + let mut condition = recursive_prepare_condition::(context, filter)?; + if !condition.is_empty() { + condition = condition.add(if let Some(via) = via.clone() { + via + } else { + to.clone() + }); + let mut subquery = R::find() + .select_only() + .expr(Expr::cust("1")) + .filter(condition) + .into_query(); + if via.is_some() { + // join the junction table + subquery.inner_join(to.from_tbl.clone(), to.clone()); + } + Ok(Some(Expr::exists(subquery))) + } else { + Ok(None) + } + }), + } + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index 4c9f9f15..c82aa7ac 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -13,6 +13,9 @@ pub use pagination::*; pub mod filtering; pub use filtering::*; +pub mod having; +pub use having::*; + pub mod entity_object_relation; pub use entity_object_relation::*; From 448f3459cebf689fee300f82dae645638d0ac7ea Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 27 Sep 2025 22:15:22 +0100 Subject: [PATCH 10/85] clippy --- macros/src/custom_input_type.rs | 2 +- macros/src/util.rs | 6 ++---- src/custom/aux_types.rs | 2 +- src/custom/input.rs | 2 +- src/query/pagination.rs | 3 +-- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/macros/src/custom_input_type.rs b/macros/src/custom_input_type.rs index d418b274..d15c8722 100644 --- a/macros/src/custom_input_type.rs +++ b/macros/src/custom_input_type.rs @@ -16,7 +16,7 @@ pub fn expand(derive_input: DeriveInput) -> syn::Result { let name: TokenStream = match &args.input_type_name { Some(name) => quote! { #name }, None => { - let name = format!("{}Input", ident); + let name = format!("{ident}Input"); quote! { #name } } }; diff --git a/macros/src/util.rs b/macros/src/util.rs index d3c05c22..4f7512e2 100644 --- a/macros/src/util.rs +++ b/macros/src/util.rs @@ -39,8 +39,7 @@ pub fn parse_enum_variants(ast: &DeriveInput, data: &DataEnum) -> Result Result(); - async_graphql::Error::new(format!("internal: field \"{}\" not found", type_name)) + async_graphql::Error::new(format!("internal: field \"{type_name}\" not found")) })? .list()? .iter() diff --git a/src/custom/input.rs b/src/custom/input.rs index 76fd7f35..424f50dd 100644 --- a/src/custom/input.rs +++ b/src/custom/input.rs @@ -101,7 +101,7 @@ where Ok(res) } value => Err(SeaographyError::AsyncGraphQLError( - format!("Expected a list, got {:?}", value).into(), + format!("Expected a list, got {value:?}").into(), )), }, } diff --git a/src/query/pagination.rs b/src/query/pagination.rs index 76e48892..060ff9d2 100644 --- a/src/query/pagination.rs +++ b/src/query/pagination.rs @@ -489,8 +489,7 @@ fn check_limit( if requested_limit > max_limit { return Err(sea_orm::DbErr::Query(sea_orm::RuntimeErr::Internal( format!( - "Requested pagination limit ({}) exceeds maximum allowed ({})", - requested_limit, max_limit + "Requested pagination limit ({requested_limit}) exceeds maximum allowed ({max_limit})" ), ))); } From 67c94bfff4a6c38b9951edd6c3c4aa044c95bda7 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 27 Sep 2025 22:43:29 +0100 Subject: [PATCH 11/85] Changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 433368b3..f917bc50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 2.0.0 - pending + +### New Features + +* Filter by related entity +```graphql +{ + country(having: { city: { city: { eq: "London" } } }) { + nodes { + country + city { + nodes { + city + } + } + } + } +} +``` + ## 1.1.5 - pending ### New Features From a8ff6e6994d2426763e7a67efe57b401c0f31752 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 27 Sep 2025 22:57:06 +0100 Subject: [PATCH 12/85] Fix CI --- examples/sea-draw/src/schema.rs | 6 ++- examples/sqlite/tests/query_tests.rs | 61 ++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/examples/sea-draw/src/schema.rs b/examples/sea-draw/src/schema.rs index a6bad0a4..2340d8d3 100644 --- a/examples/sea-draw/src/schema.rs +++ b/examples/sea-draw/src/schema.rs @@ -340,12 +340,16 @@ macro_rules! register_entity { temp }; )? + let related_entity_filter = + seaography::RelatedEntityFilter::<$module_path::Entity>::build::<$module_path::RelatedEntity>($builder.context); - $builder.register_entity::<$module_path::Entity>(fields); + $builder.register_entity::<$module_path::Entity>(fields, &related_entity_filter); $builder = $builder.register_entity_dataloader_one_to_one($module_path::Entity, tokio::spawn); $builder = $builder.register_entity_dataloader_one_to_many($module_path::Entity, tokio::spawn); + $builder = + $builder.register_related_entity_filter::<$module_path::Entity>(related_entity_filter); // Avoid using the default mutations, since we do updates and deletions on a per-row basis, // generate ids on the server side at creation time, set created_at/updated_at fields // on the server side to ensure they're accurate, implement soft deletes, and perform diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index da9f35d6..be43db64 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -1190,6 +1190,7 @@ async fn film_having_actor() { r#" { film( + filters: { filmId: { ne: 2 } } having: { actor: { firstName: { eq: "BOB" } } } orderBy: { filmId: ASC } pagination: { page: { page: 0, limit: 2 } } @@ -1214,53 +1215,77 @@ async fn film_having_actor() { "film": { "nodes": [ { - "filmId": 2, - "title": "ACE GOLDFINGER", + "filmId": 3, + "title": "ADAPTATION HOLES", "actor": { "nodes": [ + { + "firstName": "NICK", + "lastName": "WAHLBERG" + }, { "firstName": "BOB", "lastName": "FAWCETT" }, { - "firstName": "MINNIE", - "lastName": "ZELLWEGER" + "firstName": "CAMERON", + "lastName": "STREEP" }, { - "firstName": "SEAN", - "lastName": "GUINESS" + "firstName": "RAY", + "lastName": "JOHANSSON" }, { - "firstName": "CHRIS", - "lastName": "DEPP" + "firstName": "JULIANNE", + "lastName": "DENCH" } ] } }, { - "filmId": 3, - "title": "ADAPTATION HOLES", + "filmId": 144, + "title": "CHINATOWN GLADIATOR", "actor": { "nodes": [ { - "firstName": "NICK", - "lastName": "WAHLBERG" + "firstName": "UMA", + "lastName": "WOOD" + }, + { + "firstName": "DAN", + "lastName": "TORN" }, { "firstName": "BOB", "lastName": "FAWCETT" }, { - "firstName": "CAMERON", - "lastName": "STREEP" + "firstName": "JUDE", + "lastName": "CRUISE" }, { - "firstName": "RAY", - "lastName": "JOHANSSON" + "firstName": "JESSICA", + "lastName": "BAILEY" }, { - "firstName": "JULIANNE", - "lastName": "DENCH" + "firstName": "SEAN", + "lastName": "WILLIAMS" + }, + { + "firstName": "PENELOPE", + "lastName": "MONROE" + }, + { + "firstName": "GEOFFREY", + "lastName": "HESTON" + }, + { + "firstName": "JEFF", + "lastName": "SILVERSTONE" + }, + { + "firstName": "JAYNE", + "lastName": "SILVERSTONE" } ] } From bafc7e7f3dc0bcf3409d15aeb390e5534ec419b1 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 27 Sep 2025 23:08:13 +0100 Subject: [PATCH 13/85] comment --- src/query/having.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/query/having.rs b/src/query/having.rs index 04807368..21be8948 100644 --- a/src/query/having.rs +++ b/src/query/having.rs @@ -137,6 +137,12 @@ impl RelatedEntityFilterField { filter_condition_fn: Box::new(move |context, filter| -> SeaResult> { let mut condition = recursive_prepare_condition::(context, filter)?; if !condition.is_empty() { + // WHERE EXISTS( + // SELECT 1 FROM "actor" + // INNER JOIN "film_actor" ON "film_actor"."actor_id" = "actor"."actor_id" <- junction table, if applicable + // WHERE film_actor.film_id = film.film_id <- join condition + // AND actor.first_name = 'BOB' <- filter condition + // ) condition = condition.add(if let Some(via) = via.clone() { via } else { From 8171d4f61350faeb4ddbcea8693f3cf4a180860e Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 30 Sep 2025 15:56:09 +0100 Subject: [PATCH 14/85] update build script --- build-tools/bump-version.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/build-tools/bump-version.sh b/build-tools/bump-version.sh index 323c1f80..1c7ed7b8 100644 --- a/build-tools/bump-version.sh +++ b/build-tools/bump-version.sh @@ -4,22 +4,17 @@ set -e # Bump `seaography-generator` version cd generator sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml -git commit -am "seaography-generator $1" cd .. -sleep 1 # Bump `seaography-cli` version cd cli sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml sed -i 's/^seaography-generator [^,]*,/seaography-generator = { version = "~'$1'",/' Cargo.toml -git commit -am "seaography-cli $1" cd .. -sleep 1 # Bump `seaography` version sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml git commit -am "$1" -sleep 1 # Bump examples' dependency version cd examples From e09a78d6a2558b960bda8b8eb4b32b58a669775f Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 30 Sep 2025 15:56:56 +0100 Subject: [PATCH 15/85] 2.0.0-rc.2 --- Cargo.toml | 4 ++-- build-tools/bump-version.sh | 6 ++++++ cli/Cargo.toml | 4 ++-- generator/Cargo.toml | 2 +- macros/Cargo.toml | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0b6eaf89..7aff7e0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "2.0.0-rc.1" +version = "2.0.0-rc.2" edition = "2021" rust-version = "1.70" authors = [ @@ -21,7 +21,7 @@ categories = ["database"] [dependencies] async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } -seaography-macros = { version = "0.1.0", path = "macros", optional = true } +seaography-macros = { version = "~2.0.0-rc.2", path = "macros", optional = true } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } diff --git a/build-tools/bump-version.sh b/build-tools/bump-version.sh index 1c7ed7b8..73d5e499 100644 --- a/build-tools/bump-version.sh +++ b/build-tools/bump-version.sh @@ -12,6 +12,12 @@ sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml sed -i 's/^seaography-generator [^,]*,/seaography-generator = { version = "~'$1'",/' Cargo.toml cd .. +# Bump `seaography-macros` version +cd macros +sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml +sed -i 's/^seaography-macros [^,]*,/seaography-macros = { version = "~'$1'",/' ../Cargo.toml +cd .. + # Bump `seaography` version sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml git commit -am "$1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7885c0a9..2fb6e158 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-cli" -version = "2.0.0-rc.1" +version = "2.0.0-rc.2" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } clap = { version = "4.3.19", features = ["derive"] } -seaography-generator = { version = "~2.0.0-rc", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc.2", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/generator/Cargo.toml b/generator/Cargo.toml index e723c356..babe149c 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-generator" -version = "2.0.0-rc.1" +version = "2.0.0-rc.2" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 18f37d78..f91c7f66 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-macros" -version = "0.1.0" +version = "2.0.0-rc.2" edition = "2024" [lib] From d5d8a1eee31db47499ef2933057d3aee969afe48 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 30 Sep 2025 15:56:56 +0100 Subject: [PATCH 16/85] update examples --- examples/mysql/Cargo.toml | 4 ++-- examples/postgres/Cargo.toml | 4 ++-- examples/sea-draw/Cargo.toml | 4 ++-- examples/sqlite/Cargo.toml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index 6ce3d13b..e34034a6 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-mysql-example" -version = "2.0.0-rc.1" +version = "2.0.0-rc.2" [dependencies] axum = { version = "0.7" } @@ -14,7 +14,7 @@ tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc" # seaography version +version = "~2.0.0-rc.2" # seaography version features = ["with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 0b735bf4..49e34df3 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "2.0.0-rc.1" +version = "2.0.0-rc.2" [dependencies] poem = { version = "3.0" } @@ -14,7 +14,7 @@ tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc" # seaography version +version = "~2.0.0-rc.2" # seaography version features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index 7468dd08..f88c6c7e 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-draw" -version = "0.1.0" +version = "2.0.0-rc.2" edition = "2024" [dependencies] @@ -30,7 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc" # seaography version +version = "~2.0.0-rc.2" # seaography version features = [ "macros", "field-pluralize", diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 8828d432..10bb9a56 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "2.0.0-rc.1" +version = "2.0.0-rc.2" [dependencies] actix-web = { version = "4.5", default-features = false, features = ["macros"] } @@ -17,7 +17,7 @@ serde_json = { version = "1.0.103" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc" # seaography version +version = "~2.0.0-rc.2" # seaography version features = ["macros", "with-decimal", "with-chrono"] [workspace] From 0ea1f9fa3218be894b58b48a3642b7201a649423 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 30 Sep 2025 16:09:48 +0100 Subject: [PATCH 17/85] publish seaography-macros --- build-tools/cargo-publish.sh | 4 ++++ macros/Cargo.toml | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/build-tools/cargo-publish.sh b/build-tools/cargo-publish.sh index cbb62000..db3a5e9d 100644 --- a/build-tools/cargo-publish.sh +++ b/build-tools/cargo-publish.sh @@ -9,4 +9,8 @@ cd cli cargo publish cd .. +cd macros +cargo publish +cd .. + cargo publish \ No newline at end of file diff --git a/macros/Cargo.toml b/macros/Cargo.toml index f91c7f66..2f086ba6 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -2,6 +2,15 @@ name = "seaography-macros" version = "2.0.0-rc.2" edition = "2024" +rust-version = "1.85" +authors = ["Chris Tsang "] +description = "Macros for Seaography" +license = "MIT OR Apache-2.0" +homepage = "https://www.sea-ql.org/Seaography" +documentation = "https://docs.rs/seaography" +repository = "https://github.com/SeaQL/seaography" +keywords = ["async", "graphql", "mysql", "postgres", "sqlite"] +categories = ["database"] [lib] proc-macro = true From 738ec3e6ef28e387d1bb1e65a69db91b9e42e9c6 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 1 Oct 2025 12:00:57 +0100 Subject: [PATCH 18/85] Regenerate example projects with more likely web-framework choice Postgres + Axum: canonical SQLite + Poem: simple MySQL + Actix: classic --- .github/workflows/tests.yaml | 6 +-- README.md | 4 +- examples/mysql/Cargo.toml | 4 +- examples/mysql/src/main.rs | 43 ++++++++++++++-------- examples/postgres/Cargo.toml | 4 +- examples/postgres/src/main.rs | 21 ++++++----- examples/postgres/src/query_root.rs | 20 +++++----- examples/sqlite/Cargo.toml | 16 ++++---- examples/sqlite/src/main.rs | 45 ++++++++--------------- examples/sqlite/src/query_root/types.rs | 3 +- examples/sqlite/tests/more_query_tests.rs | 3 +- generator/src/templates/axum.rs | 4 +- generator/src/writer.rs | 2 +- src/lib.rs | 4 +- 14 files changed, 90 insertions(+), 89 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0289223f..8a975da6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -82,7 +82,7 @@ jobs: args: > --package seaography-cli -- ./examples/sqlite ./examples/sqlite/src/entities sqlite://sakila.db - seaography-sqlite-example -f actix + seaography-sqlite-example -f poem - name: Depends on local seaography run: >- sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' @@ -146,7 +146,7 @@ jobs: args: > --package seaography-cli -- ./examples/mysql ./examples/mysql/src/entities mysql://sea:sea@127.0.0.1/sakila - seaography-mysql-example -f axum + seaography-mysql-example -f actix - name: Depends on local seaography run: >- sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' @@ -216,7 +216,7 @@ jobs: --package seaography-cli -- ./examples/postgres ./examples/postgres/src/entities postgres://sea:sea@127.0.0.1/sakila?currentSchema=public - seaography-postgres-example -f poem + seaography-postgres-example -f axum - name: Depends on local seaography run: >- sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' diff --git a/README.md b/README.md index c0d392b4..dd34f34a 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ It's easy to integrate Seaography with any web framework, but we ship with the f ### Install ```sh -cargo install sea-orm-cli@^1.1 # used to generate entities -cargo install seaography-cli@^1.1 +cargo install sea-orm-cli@^2.0.0-rc # used to generate entities +cargo install seaography-cli@^2.0.0-rc ``` ### MySQL diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index e34034a6..d5392f80 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -4,8 +4,8 @@ name = "seaography-mysql-example" version = "2.0.0-rc.2" [dependencies] -axum = { version = "0.7" } -async-graphql-axum = { version = "7.0" } +actix-web = { version = "4.5", default-features = false, features = ["macros"] } +async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" sea-orm = { version = "~2.0.0-rc", features = ["sqlx-mysql", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/examples/mysql/src/main.rs b/examples/mysql/src/main.rs index 878fc333..bc2cba4d 100644 --- a/examples/mysql/src/main.rs +++ b/examples/mysql/src/main.rs @@ -1,15 +1,13 @@ -use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; -use async_graphql_axum::GraphQL; -use axum::{ - response::{self, IntoResponse}, - routing::get, - Router, +use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer, Result}; +use async_graphql::{ + dynamic::*, + http::{playground_source, GraphQLPlaygroundConfig}, }; +use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; use dotenv::dotenv; use sea_orm::Database; use seaography::{async_graphql, lazy_static}; use std::env; -use tokio::net::TcpListener; lazy_static::lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); @@ -25,12 +23,18 @@ lazy_static::lazy_static! { }); } -async fn graphiql() -> impl IntoResponse { - response::Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) +async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() } -#[tokio::main] -async fn main() { +async fn graphql_playground() -> Result { + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT)))) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { dotenv().ok(); tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) @@ -42,9 +46,18 @@ async fn main() { let schema = seaography_mysql_example::query_root::schema(database, *DEPTH_LIMIT, *COMPLEXITY_LIMIT) .unwrap(); - let app = Router::new().route("/", get(graphiql).post_service(GraphQL::new(schema))); println!("Visit GraphQL Playground at http://{}", *URL); - axum::serve(TcpListener::bind(&*URL).await.unwrap(), app) - .await - .unwrap(); + HttpServer::new(move || { + App::new() + .app_data(Data::new(schema.clone())) + .service(web::resource("/").guard(guard::Post()).to(index)) + .service( + web::resource("/") + .guard(guard::Get()) + .to(graphql_playground), + ) + }) + .bind("127.0.0.1:8000")? + .run() + .await } diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 49e34df3..8cfde9d3 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -4,8 +4,8 @@ name = "seaography-postgres-example" version = "2.0.0-rc.2" [dependencies] -poem = { version = "3.0" } -async-graphql-poem = { version = "7.0" } +axum = { version = "0.7" } +async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" sea-orm = { version = "~2.0.0-rc", features = ["sqlx-postgres", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/examples/postgres/src/main.rs b/examples/postgres/src/main.rs index d0f34368..a0d2fa1a 100644 --- a/examples/postgres/src/main.rs +++ b/examples/postgres/src/main.rs @@ -1,10 +1,15 @@ use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; -use async_graphql_poem::GraphQL; +use async_graphql_axum::GraphQL; +use axum::{ + response::{self, IntoResponse}, + routing::get, + Router, +}; use dotenv::dotenv; -use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; use sea_orm::Database; use seaography::{async_graphql, lazy_static}; use std::env; +use tokio::net::TcpListener; lazy_static::lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); @@ -20,9 +25,8 @@ lazy_static::lazy_static! { }); } -#[handler] async fn graphql_playground() -> impl IntoResponse { - Html(playground_source(GraphQLPlaygroundConfig::new(&ENDPOINT))) + response::Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) } #[tokio::main] @@ -38,13 +42,12 @@ async fn main() { let schema = seaography_postgres_example::query_root::schema(database, *DEPTH_LIMIT, *COMPLEXITY_LIMIT) .unwrap(); - let app = Route::new().at( + let app = Router::new().route( &*ENDPOINT, - get(graphql_playground).post(GraphQL::new(schema)), + get(graphql_playground).post_service(GraphQL::new(schema)), ); println!("Visit GraphQL Playground at http://{}", *URL); - Server::new(TcpListener::bind(&*URL)) - .run(app) + axum::serve(TcpListener::bind(&*URL).await.unwrap(), app) .await - .expect("Fail to start web server"); + .unwrap(); } diff --git a/examples/postgres/src/query_root.rs b/examples/postgres/src/query_root.rs index afad6818..ec64103b 100644 --- a/examples/postgres/src/query_root.rs +++ b/examples/postgres/src/query_root.rs @@ -23,21 +23,21 @@ pub fn schema_builder( seaography::register_entities!( builder, [ - film_actor, - rental, + actor, + address, category, - staff, + city, country, + customer, film, - actor, - language, - city, - inventory, + film_actor, film_category, - customer, - store, + inventory, + language, payment, - address, + rental, + staff, + store, ] ); builder.register_enumeration::(); diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 10bb9a56..049306a2 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -4,21 +4,21 @@ name = "seaography-sqlite-example" version = "2.0.0-rc.2" [dependencies] -actix-web = { version = "4.5", default-features = false, features = ["macros"] } -async-graphql = { version = "7.0", features = ["dataloader", "dynamic-schema"] } -async-graphql-actix-web = { version = "7.0" } +poem = { version = "3.0" } +async-graphql-poem = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["sqlx-sqlite", "runtime-async-std-rustls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["sqlx-sqlite", "runtime-async-std-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } -serde = { version = "1", features = ["derive"] } -serde_json = { version = "1.0.103" } [dependencies.seaography] -path = "../../" version = "~2.0.0-rc.2" # seaography version -features = ["macros", "with-decimal", "with-chrono"] +features = ["with-decimal", "with-chrono"] + +[dev-dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0.103" } [workspace] members = [] diff --git a/examples/sqlite/src/main.rs b/examples/sqlite/src/main.rs index 52da0a8c..37ea9085 100644 --- a/examples/sqlite/src/main.rs +++ b/examples/sqlite/src/main.rs @@ -1,10 +1,7 @@ -use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer, Result}; -use async_graphql::{ - dynamic::*, - http::{playground_source, GraphQLPlaygroundConfig}, -}; -use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; +use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; +use async_graphql_poem::GraphQL; use dotenv::dotenv; +use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; use sea_orm::Database; use seaography::{async_graphql, lazy_static}; use std::env; @@ -23,18 +20,13 @@ lazy_static::lazy_static! { }); } -async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() +#[handler] +async fn graphql_playground() -> impl IntoResponse { + Html(playground_source(GraphQLPlaygroundConfig::new(&ENDPOINT))) } -async fn graphql_playground() -> Result { - Ok(HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT)))) -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { +#[tokio::main] +async fn main() { dotenv().ok(); tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) @@ -46,18 +38,13 @@ async fn main() -> std::io::Result<()> { let schema = seaography_sqlite_example::query_root::schema(database, *DEPTH_LIMIT, *COMPLEXITY_LIMIT) .unwrap(); + let app = Route::new().at( + &*ENDPOINT, + get(graphql_playground).post(GraphQL::new(schema)), + ); println!("Visit GraphQL Playground at http://{}", *URL); - HttpServer::new(move || { - App::new() - .app_data(Data::new(schema.clone())) - .service(web::resource("/").guard(guard::Post()).to(index)) - .service( - web::resource("/") - .guard(guard::Get()) - .to(graphql_playground), - ) - }) - .bind("127.0.0.1:8000")? - .run() - .await + Server::new(TcpListener::bind(&*URL)) + .run(app) + .await + .expect("Fail to start web server"); } diff --git a/examples/sqlite/src/query_root/types.rs b/examples/sqlite/src/query_root/types.rs index c06d536e..5750ad5b 100644 --- a/examples/sqlite/src/query_root/types.rs +++ b/examples/sqlite/src/query_root/types.rs @@ -1,6 +1,5 @@ -use async_graphql; use sea_orm::entity::prelude::{DateTimeUtc, Decimal}; -use seaography::{CustomFields, CustomInputType, CustomOutputType}; +use seaography::{async_graphql, CustomFields, CustomInputType, CustomOutputType}; #[derive(Clone, CustomInputType)] pub struct RentalRequest { diff --git a/examples/sqlite/tests/more_query_tests.rs b/examples/sqlite/tests/more_query_tests.rs index 7438197d..9fc7ba2b 100644 --- a/examples/sqlite/tests/more_query_tests.rs +++ b/examples/sqlite/tests/more_query_tests.rs @@ -1,8 +1,7 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; use seaography::{ - async_graphql, lazy_static, BuilderContext, EntityQueryFieldConfig, PaginationInputConfig, - TypesMapConfig, + async_graphql, lazy_static, BuilderContext, PaginationInputConfig, TypesMapConfig, }; lazy_static::lazy_static! { diff --git a/generator/src/templates/axum.rs b/generator/src/templates/axum.rs index f5a4c39f..61e42e6c 100644 --- a/generator/src/templates/axum.rs +++ b/generator/src/templates/axum.rs @@ -37,7 +37,7 @@ pub fn generate_main(crate_name: &str) -> TokenStream { }); } - async fn graphiql() -> impl IntoResponse { + async fn graphql_playground() -> impl IntoResponse { response::Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) } @@ -52,7 +52,7 @@ pub fn generate_main(crate_name: &str) -> TokenStream { .await .expect("Fail to initialize database connection"); let schema = #crate_name_token::query_root::schema(database, *DEPTH_LIMIT, *COMPLEXITY_LIMIT).unwrap(); - let app = Router::new().route("/", get(graphiql).post_service(GraphQL::new(schema))); + let app = Router::new().route("/", get(graphql_playground).post_service(GraphQL::new(schema))); println!("Visit GraphQL Playground at http://{}", *URL); axum::serve(TcpListener::bind(&*URL).await.unwrap(), app) .await diff --git a/generator/src/writer.rs b/generator/src/writer.rs index 875558e7..a6756450 100644 --- a/generator/src/writer.rs +++ b/generator/src/writer.rs @@ -105,7 +105,7 @@ pub fn generate_query_root>(entities_path: &P) -> TokenStream { depth: Option, complexity: Option, ) -> SchemaBuilder { - let mut builder = Builder::new(&context, database.clone()); + let mut builder = Builder::new(context, database.clone()); seaography::register_entities!( builder, diff --git a/src/lib.rs b/src/lib.rs index da02afcf..0cae81bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,8 +64,8 @@ //! ### Install //! //! ```sh -//! cargo install sea-orm-cli@^1.1 # used to generate entities -//! cargo install seaography-cli@^1.1 +//! cargo install sea-orm-cli@^2.0.0-rc # used to generate entities +//! cargo install seaography-cli@^2.0.0-rc //! ``` //! //! ### MySQL From bba9124db94811baaa8630b3cc21bdba6f324562 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 2 Oct 2025 18:33:32 +0100 Subject: [PATCH 19/85] Readme --- README.md | 4 ++-- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dd34f34a..aa80e780 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ Seaography is built on top of SeaORM, so it supports: It's easy to integrate Seaography with any web framework, but we ship with the following examples out-of-the-box: -+ [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres) -+ [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) ++ [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite) ++ [Loco (SeaORM)](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_seaography), [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) ## Features diff --git a/src/lib.rs b/src/lib.rs index 0cae81bc..2511cbb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,8 +38,8 @@ //! //! It's easy to integrate Seaography with any web framework, but we ship with the following examples out-of-the-box: //! -//! + [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres) -//! + [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) +//! + [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite) +//! + [Loco (SeaORM)](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_seaography), [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) //! //! ## Features //! From 77936b656b63419acc73bbeb9256ae2d68d11f4c Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 4 Oct 2025 19:00:30 +0100 Subject: [PATCH 20/85] register entity without mutation --- src/builder.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index 91a896dc..9842f81b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -677,7 +677,25 @@ macro_rules! register_entity { $builder.register_entity_dataloader_one_to_many($module_path::Entity, tokio::spawn); $builder = $builder.register_related_entity_filter::<$module_path::Entity>(related_entity_filter); - $builder.register_entity_mutations::<$module_path::Entity, $module_path::ActiveModel>(); + }; + ($builder:expr, $module_path:ident, mutation: $mutation:expr) => { + let relations = + <$module_path::RelatedEntity as sea_orm::Iterable>::iter() + .map(|rel| seaography::RelationBuilder::get_relation(&rel, $builder.context)) + .collect(); + let related_entity_filter = + seaography::RelatedEntityFilter::<$module_path::Entity>::build::<$module_path::RelatedEntity>($builder.context); + + $builder.register_entity::<$module_path::Entity>(relations, &related_entity_filter); + $builder = + $builder.register_entity_dataloader_one_to_one($module_path::Entity, tokio::spawn); + $builder = + $builder.register_entity_dataloader_one_to_many($module_path::Entity, tokio::spawn); + $builder = + $builder.register_related_entity_filter::<$module_path::Entity>(related_entity_filter); + if $mutation { + $builder.register_entity_mutations::<$module_path::Entity, $module_path::ActiveModel>(); + } }; } From 0a2de445d3c8c9112645e392bf040b2ab606e6d6 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 6 Oct 2025 23:38:28 +0100 Subject: [PATCH 21/85] Fix up --- src/builder.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 9842f81b..36ded84e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -663,20 +663,7 @@ macro_rules! impl_custom_output_type_for_entity { #[macro_export] macro_rules! register_entity { ($builder:expr, $module_path:ident) => { - let relations = - <$module_path::RelatedEntity as sea_orm::Iterable>::iter() - .map(|rel| seaography::RelationBuilder::get_relation(&rel, $builder.context)) - .collect(); - let related_entity_filter = - seaography::RelatedEntityFilter::<$module_path::Entity>::build::<$module_path::RelatedEntity>($builder.context); - - $builder.register_entity::<$module_path::Entity>(relations, &related_entity_filter); - $builder = - $builder.register_entity_dataloader_one_to_one($module_path::Entity, tokio::spawn); - $builder = - $builder.register_entity_dataloader_one_to_many($module_path::Entity, tokio::spawn); - $builder = - $builder.register_related_entity_filter::<$module_path::Entity>(related_entity_filter); + seaography::register_entity!($builder, $module_path, mutation: true); }; ($builder:expr, $module_path:ident, mutation: $mutation:expr) => { let relations = From c92cd55cdfbc7704dbbf89a6358c1652286089a1 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 6 Oct 2025 23:45:32 +0100 Subject: [PATCH 22/85] Fixup --- examples/mysql/tests/mutation_tests.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/examples/mysql/tests/mutation_tests.rs b/examples/mysql/tests/mutation_tests.rs index 5e052f1e..377da10f 100644 --- a/examples/mysql/tests/mutation_tests.rs +++ b/examples/mysql/tests/mutation_tests.rs @@ -2,14 +2,6 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; use seaography::async_graphql; -async fn main() { - test_simple_insert_one().await; - test_complex_insert_one().await; - test_create_batch_mutation().await; - test_update_mutation().await; - test_delete_mutation().await; -} - async fn schema() -> Schema { let database = Database::connect("mysql://sea:sea@127.0.0.1/sakila") .await @@ -26,6 +18,7 @@ fn assert_eq(a: Response, b: &str) { ) } +#[tokio::test] async fn test_simple_insert_one() { let schema = schema().await; @@ -108,6 +101,7 @@ async fn test_simple_insert_one() { ); } +#[tokio::test] async fn test_complex_insert_one() { let schema = schema().await; @@ -214,6 +208,7 @@ async fn test_complex_insert_one() { ); } +#[tokio::test] async fn test_create_batch_mutation() { let schema = schema().await; @@ -358,6 +353,7 @@ async fn test_create_batch_mutation() { ) } +#[tokio::test] async fn test_update_mutation() { let schema = schema().await; @@ -536,6 +532,7 @@ async fn test_update_mutation() { ); } +#[tokio::test] async fn test_delete_mutation() { let schema = schema().await; From e00531453f6a5093704526390f108fb6be0f8c8a Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 8 Oct 2025 10:31:31 +0100 Subject: [PATCH 23/85] Docs --- Cargo.toml | 4 ++++ src/custom/input.rs | 18 ++++++++++++++++++ src/custom/output.rs | 22 ++++++++++++++++++++++ src/custom/types.rs | 37 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 5 files changed, 82 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7aff7e0f..91b31e8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,10 @@ repository = "https://github.com/SeaQL/seaography" keywords = ["async", "graphql", "mysql", "postgres", "sqlite"] categories = ["database"] +[package.metadata.docs.rs] +features = ["default"] +rustdoc-args = ["--cfg", "docsrs"] + [dependencies] async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } diff --git a/src/custom/input.rs b/src/custom/input.rs index 424f50dd..13584876 100644 --- a/src/custom/input.rs +++ b/src/custom/input.rs @@ -4,6 +4,24 @@ use async_graphql::{ Upload, }; #[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +/// ``` +/// use seaography::CustomInputType; +/// +/// #[derive(Clone, CustomInputType)] +/// pub struct RentalRequest { +/// pub customer: String, +/// pub film: String, +/// pub location: Option, +/// pub membership_id: i32, +/// } +/// +/// #[derive(Clone, CustomInputType)] +/// pub struct Location { +/// pub city: String, +/// pub county: Option, +/// } +/// ``` pub use seaography_macros::CustomInputType; pub trait CustomInputType: Sized { diff --git a/src/custom/output.rs b/src/custom/output.rs index 9cd040ff..c69937cb 100644 --- a/src/custom/output.rs +++ b/src/custom/output.rs @@ -4,6 +4,28 @@ use crate::{ }; use async_graphql::dynamic::{FieldValue, Object, TypeRef}; #[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +/// ``` +/// use seaography::CustomOutputType; +/// +/// #[derive(Clone, CustomOutputType)] +/// pub struct PurchaseOrder { +/// pub po_number: String, +/// pub lineitems: Vec, +/// } +/// +/// #[derive(Clone, CustomOutputType)] +/// pub struct Lineitem { +/// pub product: String, +/// pub quantity: f64, +/// pub size: Option, +/// } +/// +/// #[derive(Clone, CustomOutputType)] +/// pub struct ProductSize { +/// pub size: i32, +/// } +/// ``` pub use seaography_macros::{ConvertOutput, CustomOutputType}; pub trait CustomOutputType { diff --git a/src/custom/types.rs b/src/custom/types.rs index 2643182b..043c50d7 100644 --- a/src/custom/types.rs +++ b/src/custom/types.rs @@ -6,6 +6,43 @@ use crate::{ use async_graphql::dynamic::{Enum, Field, FieldValue, TypeRef, Union, ValueAccessor}; use sea_orm::{EntityTrait, ModelTrait, TryIntoModel}; #[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +/// ``` +/// use seaography::{async_graphql, CustomFields, CustomInputType}; +/// use async_graphql::Context; +/// +/// pub struct Operations; +/// +/// #[CustomFields] +/// impl Operations { +/// async fn foo(_ctx: &Context<'_>, username: String) -> async_graphql::Result { +/// Ok(format!("Hello, {}!", username)) +/// } +/// +/// async fn bar(_ctx: &Context<'_>, x: i32, y: i32) -> async_graphql::Result { +/// Ok(x + y) +/// } +/// } +/// +/// #[derive(Clone, CustomInputType)] +/// pub struct Circle { +/// pub center: Point, +/// pub radius: f64, +/// } +/// +/// #[derive(Clone, Copy, CustomInputType)] +/// pub struct Point { +/// pub x: f64, +/// pub y: f64, +/// } +/// +/// #[CustomFields] +/// impl Circle { +/// pub async fn area(&self) -> async_graphql::Result { +/// Ok(std::f64::consts::PI * self.radius * self.radius) +/// } +/// } +/// ``` pub use seaography_macros::{CustomEnum, CustomFields}; pub trait CustomFields { diff --git a/src/lib.rs b/src/lib.rs index 2511cbb0..502a6f25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -301,6 +301,7 @@ pub mod rbac; pub use rbac::*; #[cfg(feature = "schema-meta")] +#[cfg_attr(docsrs, doc(cfg(feature = "schema-meta")))] pub mod schema; pub type SimpleNamingFn = Box String + Sync + Send>; From 5da544900e65af2a269965c22d7fa313a6a9907c Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 8 Oct 2025 10:53:44 +0100 Subject: [PATCH 24/85] Docs --- src/builder.rs | 16 +++++++++++----- src/custom/input.rs | 4 ++-- src/custom/output.rs | 4 ++-- src/custom/types.rs | 14 ++++++++++++-- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 36ded84e..aad2a76a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -108,7 +108,8 @@ impl Builder { } } - /// used to register a new entity to the Builder context + /// Register a SeaORM Entity to the schema. + /// With all the edge, connection and filter objects. pub fn register_entity( &mut self, relations: Vec, @@ -171,8 +172,8 @@ impl Builder { self.metadata.insert(T::default().to_string(), metadata); } - /// register a custom entity that only has the model for input / ouput. - /// no query / mutation will be added. intended for use in custom operations. + /// Register a SeaORM entity to use the Model for input / ouput. + /// No query / mutation will be added. Intended for use in custom operations. pub fn register_custom_entity(&mut self) where T: EntityTrait, @@ -292,7 +293,7 @@ impl Builder { self } - /// used to register a new enumeration to the builder context + /// Used to register an SeaORM ActiveEnum to the schema pub fn register_enumeration(&mut self) where A: ActiveEnum, @@ -336,6 +337,7 @@ impl Builder { self.inputs.push(T::input_object(self.context)); } + /// Register a simple object as custom output pub fn register_custom_output(&mut self) where T: CustomOutputObject, @@ -343,6 +345,7 @@ impl Builder { self.outputs.push(T::basic_object(self.context)); } + /// Register a custom output object without custom fields pub fn register_complex_custom_output(&mut self) where T: CustomOutputObject + CustomFields, @@ -438,7 +441,8 @@ impl Builder { self.queries.push(field); } - /// used to consume the builder context and generate a ready to be completed GraphQL schema + /// Consume the builder context and generate a ready to be completed GraphQL schema. + /// You can extend the schema, or attach additional data to it before finish(). pub fn schema_builder(mut self) -> SchemaBuilder { #[cfg(feature = "schema-meta")] self.register_schema_meta(); @@ -661,6 +665,8 @@ macro_rules! impl_custom_output_type_for_entity { } #[macro_export] +/// Includes mutations by default. Can skip mutations by +/// `seaography::register_entity!(builder, my_entity, mutation: false);`. macro_rules! register_entity { ($builder:expr, $module_path:ident) => { seaography::register_entity!($builder, $module_path, mutation: true); diff --git a/src/custom/input.rs b/src/custom/input.rs index 13584876..3837cc40 100644 --- a/src/custom/input.rs +++ b/src/custom/input.rs @@ -5,6 +5,8 @@ use async_graphql::{ }; #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +pub use seaography_macros::CustomInputType; + /// ``` /// use seaography::CustomInputType; /// @@ -22,8 +24,6 @@ use async_graphql::{ /// pub county: Option, /// } /// ``` -pub use seaography_macros::CustomInputType; - pub trait CustomInputType: Sized { fn gql_input_type_ref(ctx: &'static BuilderContext) -> TypeRef; diff --git a/src/custom/output.rs b/src/custom/output.rs index c69937cb..d1fcd5ea 100644 --- a/src/custom/output.rs +++ b/src/custom/output.rs @@ -5,6 +5,8 @@ use crate::{ use async_graphql::dynamic::{FieldValue, Object, TypeRef}; #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +pub use seaography_macros::{ConvertOutput, CustomOutputType}; + /// ``` /// use seaography::CustomOutputType; /// @@ -26,8 +28,6 @@ use async_graphql::dynamic::{FieldValue, Object, TypeRef}; /// pub size: i32, /// } /// ``` -pub use seaography_macros::{ConvertOutput, CustomOutputType}; - pub trait CustomOutputType { fn gql_output_type_ref(ctx: &'static BuilderContext) -> TypeRef; fn gql_field_value(self, ctx: &'static BuilderContext) -> Option>; diff --git a/src/custom/types.rs b/src/custom/types.rs index 043c50d7..9cd77fa2 100644 --- a/src/custom/types.rs +++ b/src/custom/types.rs @@ -7,6 +7,8 @@ use async_graphql::dynamic::{Enum, Field, FieldValue, TypeRef, Union, ValueAcces use sea_orm::{EntityTrait, ModelTrait, TryIntoModel}; #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +pub use seaography_macros::{CustomEnum, CustomFields}; + /// ``` /// use seaography::{async_graphql, CustomFields, CustomInputType}; /// use async_graphql::Context; @@ -43,12 +45,20 @@ use sea_orm::{EntityTrait, ModelTrait, TryIntoModel}; /// } /// } /// ``` -pub use seaography_macros::{CustomEnum, CustomFields}; - pub trait CustomFields { fn to_fields(context: &'static BuilderContext) -> Vec; } +/// ``` +/// use seaography::CustomEnum; +/// +/// #[derive(CustomEnum)] +/// pub enum Status { +/// Available, +/// BackOrdering, +/// Unavailable, +/// } +/// ``` pub trait CustomEnum { fn to_enum() -> Enum; } From ceb0029726528168fa40918508255343e05b46df Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Fri, 10 Oct 2025 15:06:55 +0100 Subject: [PATCH 25/85] Readme --- README.md | 3 ++- src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa80e780..648d194d 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ Find all inactive customers, include their address, and their payments with amou } ``` -### Filter using enumeration +### Filter using Postgres enum ```graphql { film( @@ -244,6 +244,7 @@ Find all inactive customers, include their address, and their payments with amou ) { nodes { filmId + title rating } } diff --git a/src/lib.rs b/src/lib.rs index 502a6f25..e275e0e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,7 +235,7 @@ //! } //! ``` //! -//! ### Filter using enumeration +//! ### Filter using Postgres enum //! ```graphql //! { //! film( @@ -244,6 +244,7 @@ //! ) { //! nodes { //! filmId +//! title //! rating //! } //! } From d8a625725596ad1f6426e3ae07706767bcd08457 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 15:07:13 +0100 Subject: [PATCH 26/85] Improve codegen 1. pickup DATABASE_URL from env 2. format lazy_static block properly 3. use sea_orm's register_entity_modules --- cli/Cargo.toml | 2 +- cli/src/main.rs | 9 +++- generator/src/lib.rs | 10 ++-- generator/src/templates/actix.rs | 4 +- generator/src/templates/axum.rs | 22 ++++++--- generator/src/templates/axum_cargo.toml | 2 +- generator/src/templates/poem.rs | 18 ++++--- generator/src/writer.rs | 62 ++++++++++--------------- 8 files changed, 69 insertions(+), 60 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2fb6e158..8f8aa8ae 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -14,6 +14,6 @@ categories = ["database"] [dependencies] async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } -clap = { version = "4.3.19", features = ["derive"] } +clap = { version = "4.3.19", features = ["env", "derive"] } seaography-generator = { version = "~2.0.0-rc.2", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index a89af24f..3cd7068d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,7 +10,14 @@ pub struct Args { /// SeaORM entities folder pub entities: String, - /// Database URL to write in .env + /// Database URL + #[arg( + short = 'u', + long, + env = "DATABASE_URL", + help = "Database URL", + hide_env_values = true + )] pub database_url: String, /// Crate name for generated project diff --git a/generator/src/lib.rs b/generator/src/lib.rs index c99f1436..8dffd921 100644 --- a/generator/src/lib.rs +++ b/generator/src/lib.rs @@ -14,9 +14,9 @@ pub enum WebFrameworkEnum { } #[allow(clippy::too_many_arguments)] -pub async fn write_project, T: AsRef>( - root_path: &P, - entities_path: &T, +pub async fn write_project( + root_path: &std::path::Path, + entities_path: &std::path::Path, db_url: &str, crate_name: &str, sql_library: &str, @@ -26,7 +26,7 @@ pub async fn write_project, T: AsRef> ) -> Result<()> { writer::write_cargo_toml(root_path, crate_name, sql_library, framework)?; - let src_path = &root_path.as_ref().join("src"); + let src_path = &root_path.join("src"); writer::write_query_root(src_path, entities_path)?; writer::write_lib(src_path)?; @@ -37,7 +37,7 @@ pub async fn write_project, T: AsRef> WebFrameworkEnum::Axum => crate::templates::axum::write_main(src_path, crate_name)?, } - writer::write_env(&root_path.as_ref(), db_url, depth_limit, complexity_limit)?; + writer::write_env(root_path, db_url, depth_limit, complexity_limit)?; std::process::Command::new("cargo") .arg("fmt") diff --git a/generator/src/templates/actix.rs b/generator/src/templates/actix.rs index 70e9911a..a5225475 100644 --- a/generator/src/templates/actix.rs +++ b/generator/src/templates/actix.rs @@ -18,10 +18,10 @@ pub fn generate_main(crate_name: &str) -> TokenStream { use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; use dotenv::dotenv; use sea_orm::Database; - use seaography::{async_graphql, lazy_static}; + use seaography::{async_graphql, lazy_static::lazy_static}; use std::env; - lazy_static::lazy_static! { + lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); static ref DATABASE_URL: String = diff --git a/generator/src/templates/axum.rs b/generator/src/templates/axum.rs index 61e42e6c..10f9b975 100644 --- a/generator/src/templates/axum.rs +++ b/generator/src/templates/axum.rs @@ -10,20 +10,21 @@ pub fn generate_main(crate_name: &str) -> TokenStream { let crate_name_token: TokenStream = crate_name.replace('-', "_").parse().unwrap(); quote! { - use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; - use async_graphql_axum::GraphQL; + use async_graphql::{dynamic::Schema, http::{playground_source, GraphQLPlaygroundConfig}}; + use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::{ + extract::State, response::{self, IntoResponse}, routing::get, Router, }; use dotenv::dotenv; use sea_orm::Database; - use seaography::{async_graphql, lazy_static}; + use seaography::{async_graphql, lazy_static::lazy_static}; use std::env; use tokio::net::TcpListener; - lazy_static::lazy_static! { + lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); static ref DATABASE_URL: String = @@ -41,6 +42,11 @@ pub fn generate_main(crate_name: &str) -> TokenStream { response::Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) } + async fn graphql_handler(State(schema): State, req: GraphQLRequest) -> GraphQLResponse { + let req = req.into_inner(); + schema.execute(req).await.into() + } + #[tokio::main] async fn main() { dotenv().ok(); @@ -48,11 +54,13 @@ pub fn generate_main(crate_name: &str) -> TokenStream { .with_max_level(tracing::Level::INFO) .with_test_writer() .init(); - let database = Database::connect(&*DATABASE_URL) + let db = Database::connect(&*DATABASE_URL) .await .expect("Fail to initialize database connection"); - let schema = #crate_name_token::query_root::schema(database, *DEPTH_LIMIT, *COMPLEXITY_LIMIT).unwrap(); - let app = Router::new().route("/", get(graphql_playground).post_service(GraphQL::new(schema))); + let schema = #crate_name_token::query_root::schema(db, *DEPTH_LIMIT, *COMPLEXITY_LIMIT).unwrap(); + let app = Router::new() + .route(&*ENDPOINT, get(graphql_playground).post(graphql_handler)) + .with_state(schema); println!("Visit GraphQL Playground at http://{}", *URL); axum::serve(TcpListener::bind(&*URL).await.unwrap(), app) .await diff --git a/generator/src/templates/axum_cargo.toml b/generator/src/templates/axum_cargo.toml index 57edee9e..90e93ff9 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -4,7 +4,7 @@ name = "" version = "0.1.0" [dependencies] -axum = { version = "0.7" } +axum = { version = "0.8" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-async-std-native-tls", "seaography"] } diff --git a/generator/src/templates/poem.rs b/generator/src/templates/poem.rs index 6d01455d..24f0890d 100644 --- a/generator/src/templates/poem.rs +++ b/generator/src/templates/poem.rs @@ -10,15 +10,15 @@ pub fn generate_main(crate_name: &str) -> TokenStream { let crate_name_token: TokenStream = crate_name.replace('-', "_").parse().unwrap(); quote! { - use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; - use async_graphql_poem::GraphQL; + use async_graphql::{dynamic::Schema, http::{playground_source, GraphQLPlaygroundConfig}}; + use async_graphql_poem::{GraphQLRequest, GraphQLResponse}; use dotenv::dotenv; - use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; + use poem::{get, handler, listener::TcpListener, EndpointExt, IntoResponse, Route, Server, web::{Data, Html}}; use sea_orm::Database; - use seaography::{async_graphql, lazy_static}; + use seaography::{async_graphql, lazy_static::lazy_static}; use std::env; - lazy_static::lazy_static! { + lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); static ref DATABASE_URL: String = @@ -37,6 +37,12 @@ pub fn generate_main(crate_name: &str) -> TokenStream { Html(playground_source(GraphQLPlaygroundConfig::new(&ENDPOINT))) } + #[handler] + async fn graphql_handler(schema: Data<&Schema>, req: GraphQLRequest) -> GraphQLResponse { + let req = req.0; + schema.execute(req).await.into() + } + #[tokio::main] async fn main() { dotenv().ok(); @@ -50,7 +56,7 @@ pub fn generate_main(crate_name: &str) -> TokenStream { let schema = #crate_name_token::query_root::schema(database, *DEPTH_LIMIT, *COMPLEXITY_LIMIT).unwrap(); let app = Route::new().at( &*ENDPOINT, - get(graphql_playground).post(GraphQL::new(schema)), + get(graphql_playground).post(graphql_handler).data(schema), ); println!("Visit GraphQL Playground at http://{}", *URL); Server::new(TcpListener::bind(&*URL)) diff --git a/generator/src/writer.rs b/generator/src/writer.rs index a6756450..18d1792d 100644 --- a/generator/src/writer.rs +++ b/generator/src/writer.rs @@ -9,7 +9,7 @@ use crate::{ WebFrameworkEnum, }; -pub fn generate_query_root>(entities_path: &P) -> TokenStream { +pub fn generate_query_root(entities_path: &Path) -> TokenStream { let mut entities_paths: Vec<_> = std::fs::read_dir(entities_path) .unwrap() .filter(|r| r.is_ok()) @@ -37,7 +37,7 @@ pub fn generate_query_root>(entities_path: &P) -> TokenStream { }) .collect(); - let entities: Vec = entities + let _entities: Vec = entities .iter() .map(|entity| { let entity_path = &entity.name; @@ -62,32 +62,28 @@ pub fn generate_query_root>(entities_path: &P) -> TokenStream { } }); - let enumerations = match enumerations { - Some(_) => { - let file_content = - std::fs::read_to_string(entities_path.as_ref().join("sea_orm_active_enums.rs")) - .unwrap(); + let enumerations = if enumerations.is_some() { + let file_content = + std::fs::read_to_string(entities_path.join("sea_orm_active_enums.rs")).unwrap(); - parse_enumerations(file_content) - } - None => vec![], + Some(parse_enumerations(file_content)) + } else { + None }; - let enumerations = enumerations.iter().map(|definition| { - let name = &definition.name; - - quote! { - builder.register_enumeration::(); - } - }); + let enumerations = if enumerations.is_some() { + quote!(builder = register_active_enums(builder);) + } else { + quote!() + }; quote! { use crate::entities::*; use async_graphql::dynamic::*; use sea_orm::DatabaseConnection; - use seaography::{async_graphql, lazy_static, Builder, BuilderContext}; + use seaography::{async_graphql, lazy_static::lazy_static, Builder, BuilderContext}; - lazy_static::lazy_static! { + lazy_static! { static ref CONTEXT: BuilderContext = BuilderContext::default(); } @@ -107,14 +103,9 @@ pub fn generate_query_root>(entities_path: &P) -> TokenStream { ) -> SchemaBuilder { let mut builder = Builder::new(context, database.clone()); - seaography::register_entities!( - builder, - [ - #(#entities,)* - ] - ); + builder = register_entity_modules(builder); - #(#enumerations)* + #enumerations builder .set_depth_limit(depth) @@ -125,13 +116,10 @@ pub fn generate_query_root>(entities_path: &P) -> TokenStream { } } -pub fn write_query_root, T: AsRef>( - src_path: &P, - entities_path: &T, -) -> Result<(), crate::error::Error> { +pub fn write_query_root(src_path: &Path, entities_path: &Path) -> Result<(), crate::error::Error> { let tokens = generate_query_root(entities_path); - let file_name = src_path.as_ref().join("query_root.rs"); + let file_name = src_path.join("query_root.rs"); std::fs::write(file_name, add_line_break(tokens))?; @@ -141,13 +129,13 @@ pub fn write_query_root, T: AsRef>( /// /// Used to generate project/Cargo.toml file content /// -pub fn write_cargo_toml>( - path: &P, +pub fn write_cargo_toml( + path: &Path, crate_name: &str, sql_library: &str, framework: WebFrameworkEnum, ) -> std::io::Result<()> { - let file_path = path.as_ref().join("Cargo.toml"); + let file_path = path.join("Cargo.toml"); let ver = env!("CARGO_PKG_VERSION"); @@ -186,8 +174,8 @@ pub fn write_lib>(path: &P) -> std::io::Result<()> { /// /// Used to generate project/.env file content /// -pub fn write_env>( - path: &P, +pub fn write_env( + path: &Path, db_url: &str, depth_limit: Option, complexity_limit: Option, @@ -202,7 +190,7 @@ pub fn write_env>( ] .join("\n"); - let file_name = path.as_ref().join(".env"); + let file_name = path.join(".env"); std::fs::write(file_name, tokens)?; From 7196ebb037cb1bbb60dd367934772ee42827c6f8 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 17:33:54 +0100 Subject: [PATCH 27/85] Ignore fields with custom type --- src/builder_context/filter_types_map.rs | 2 +- src/builder_context/types_map.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builder_context/filter_types_map.rs b/src/builder_context/filter_types_map.rs index eb1a0623..63ef7391 100644 --- a/src/builder_context/filter_types_map.rs +++ b/src/builder_context/filter_types_map.rs @@ -291,7 +291,7 @@ impl FilterTypesMapHelper { } } ColumnType::Uuid => Some(FilterType::Text), - ColumnType::Custom(name) => Some(FilterType::Custom(name.to_string())), + ColumnType::Custom(_) => None, ColumnType::Enum { name, variants: _ } => { Some(FilterType::Enumeration(name.to_string())) } diff --git a/src/builder_context/types_map.rs b/src/builder_context/types_map.rs index 873b1fea..e8f58bee 100644 --- a/src/builder_context/types_map.rs +++ b/src/builder_context/types_map.rs @@ -360,7 +360,7 @@ impl TypesMapHelper { ); iden_type.map(|it| TypeRef::List(Box::new(it))) } - ColumnType::Custom(_iden) => Some(TypeRef::named(TypeRef::STRING)), + ColumnType::Custom(_) => None, _ => None, }, } From 46c0e024a068f6f6285b3de6231576e04ca7fe45 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 17:33:34 +0100 Subject: [PATCH 28/85] update postgres example --- examples/postgres/Cargo.toml | 2 +- examples/postgres/src/entities/film.rs | 1 + examples/postgres/src/entities/mod.rs | 19 ++++++++++++ .../src/entities/sea_orm_active_enums.rs | 4 +-- examples/postgres/src/entities/staff.rs | 15 +++++++++- examples/postgres/src/entities/store.rs | 1 + examples/postgres/src/main.rs | 28 +++++++++++------- examples/postgres/src/query_root.rs | 29 ++++--------------- 8 files changed, 62 insertions(+), 37 deletions(-) diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 8cfde9d3..65eee302 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -4,7 +4,7 @@ name = "seaography-postgres-example" version = "2.0.0-rc.2" [dependencies] -axum = { version = "0.7" } +axum = { version = "0.8" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" sea-orm = { version = "~2.0.0-rc", features = ["sqlx-postgres", "runtime-async-std-native-tls", "seaography"] } diff --git a/examples/postgres/src/entities/film.rs b/examples/postgres/src/entities/film.rs index 598b22bc..8abc5062 100644 --- a/examples/postgres/src/entities/film.rs +++ b/examples/postgres/src/entities/film.rs @@ -21,6 +21,7 @@ pub struct Model { pub rating: Option, pub last_update: DateTime, pub special_features: Option>, + #[sea_orm(column_type = "JsonBinary", nullable)] pub metadata: Option, } diff --git a/examples/postgres/src/entities/mod.rs b/examples/postgres/src/entities/mod.rs index cb574fbe..2c373dd3 100644 --- a/examples/postgres/src/entities/mod.rs +++ b/examples/postgres/src/entities/mod.rs @@ -16,3 +16,22 @@ pub mod rental; pub mod sea_orm_active_enums; pub mod staff; pub mod store; + +seaography::register_entity_modules!([ + actor, + address, + category, + city, + country, + customer, + film, + film_actor, + film_category, + inventory, + language, + payment, + rental, + staff, + store, +]); +seaography::register_active_enums!([sea_orm_active_enums::MpaaRating,]); diff --git a/examples/postgres/src/entities/sea_orm_active_enums.rs b/examples/postgres/src/entities/sea_orm_active_enums.rs index 85faa065..94dde1a8 100644 --- a/examples/postgres/src/entities/sea_orm_active_enums.rs +++ b/examples/postgres/src/entities/sea_orm_active_enums.rs @@ -5,12 +5,12 @@ use sea_orm::entity::prelude::*; pub enum MpaaRating { #[sea_orm(string_value = "G")] G, - #[sea_orm(string_value = "NC-17")] - Nc17, #[sea_orm(string_value = "PG")] Pg, #[sea_orm(string_value = "PG-13")] Pg13, #[sea_orm(string_value = "R")] R, + #[sea_orm(string_value = "NC-17")] + Nc17, } diff --git a/examples/postgres/src/entities/staff.rs b/examples/postgres/src/entities/staff.rs index 60f04bdd..87ef03db 100644 --- a/examples/postgres/src/entities/staff.rs +++ b/examples/postgres/src/entities/staff.rs @@ -8,13 +8,14 @@ pub struct Model { pub first_name: String, pub last_name: String, pub address_id: i16, + pub reports_to_id: Option, pub email: Option, pub store_id: i16, pub active: bool, pub username: String, pub password: Option, pub last_update: DateTime, - #[sea_orm(column_type = "Binary(255)", nullable)] + #[sea_orm(column_type = "VarBinary(StringLen::None)", nullable)] pub picture: Option>, } @@ -32,6 +33,14 @@ pub enum Relation { Payment, #[sea_orm(has_many = "super::rental::Entity")] Rental, + #[sea_orm( + belongs_to = "Entity", + from = "Column::ReportsToId", + to = "Column::StaffId", + on_update = "NoAction", + on_delete = "NoAction" + )] + SelfRef, #[sea_orm( belongs_to = "super::store::Entity", from = "Column::StoreId", @@ -76,6 +85,10 @@ pub enum RelatedEntity { Payment, #[sea_orm(entity = "super::rental::Entity")] Rental, + #[sea_orm(entity = "Entity", def = "Relation::SelfRef.def()")] + SelfRef, #[sea_orm(entity = "super::store::Entity")] Store, + #[sea_orm(entity = "Entity", def = "Relation::SelfRef.def().rev()")] + SelfRefReverse, } diff --git a/examples/postgres/src/entities/store.rs b/examples/postgres/src/entities/store.rs index b08b44c9..44552fe0 100644 --- a/examples/postgres/src/entities/store.rs +++ b/examples/postgres/src/entities/store.rs @@ -5,6 +5,7 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub store_id: i32, + #[sea_orm(unique)] pub manager_staff_id: i16, pub address_id: i16, pub last_update: DateTime, diff --git a/examples/postgres/src/main.rs b/examples/postgres/src/main.rs index a0d2fa1a..066df05e 100644 --- a/examples/postgres/src/main.rs +++ b/examples/postgres/src/main.rs @@ -1,17 +1,21 @@ -use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; -use async_graphql_axum::GraphQL; +use async_graphql::{ + dynamic::Schema, + http::{playground_source, GraphQLPlaygroundConfig}, +}; +use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::{ + extract::State, response::{self, IntoResponse}, routing::get, Router, }; use dotenv::dotenv; use sea_orm::Database; -use seaography::{async_graphql, lazy_static}; +use seaography::{async_graphql, lazy_static::lazy_static}; use std::env; use tokio::net::TcpListener; -lazy_static::lazy_static! { +lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); static ref DATABASE_URL: String = @@ -29,6 +33,11 @@ async fn graphql_playground() -> impl IntoResponse { response::Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) } +async fn graphql_handler(State(schema): State, req: GraphQLRequest) -> GraphQLResponse { + let req = req.into_inner(); + schema.execute(req).await.into() +} + #[tokio::main] async fn main() { dotenv().ok(); @@ -36,16 +45,15 @@ async fn main() { .with_max_level(tracing::Level::INFO) .with_test_writer() .init(); - let database = Database::connect(&*DATABASE_URL) + let db = Database::connect(&*DATABASE_URL) .await .expect("Fail to initialize database connection"); let schema = - seaography_postgres_example::query_root::schema(database, *DEPTH_LIMIT, *COMPLEXITY_LIMIT) + seaography_postgres_example::query_root::schema(db, *DEPTH_LIMIT, *COMPLEXITY_LIMIT) .unwrap(); - let app = Router::new().route( - &*ENDPOINT, - get(graphql_playground).post_service(GraphQL::new(schema)), - ); + let app = Router::new() + .route(&*ENDPOINT, get(graphql_playground).post(graphql_handler)) + .with_state(schema); println!("Visit GraphQL Playground at http://{}", *URL); axum::serve(TcpListener::bind(&*URL).await.unwrap(), app) .await diff --git a/examples/postgres/src/query_root.rs b/examples/postgres/src/query_root.rs index ec64103b..6471b0ec 100644 --- a/examples/postgres/src/query_root.rs +++ b/examples/postgres/src/query_root.rs @@ -1,9 +1,11 @@ use crate::entities::*; use async_graphql::dynamic::*; use sea_orm::DatabaseConnection; -use seaography::{async_graphql, lazy_static, Builder, BuilderContext}; +use seaography::{async_graphql, lazy_static::lazy_static, Builder, BuilderContext}; -lazy_static::lazy_static! { static ref CONTEXT : BuilderContext = BuilderContext :: default () ; } +lazy_static! { + static ref CONTEXT: BuilderContext = BuilderContext::default(); +} pub fn schema( database: DatabaseConnection, @@ -20,27 +22,8 @@ pub fn schema_builder( complexity: Option, ) -> SchemaBuilder { let mut builder = Builder::new(context, database.clone()); - seaography::register_entities!( - builder, - [ - actor, - address, - category, - city, - country, - customer, - film, - film_actor, - film_category, - inventory, - language, - payment, - rental, - staff, - store, - ] - ); - builder.register_enumeration::(); + builder = register_entity_modules(builder); + builder = register_active_enums(builder); builder .set_depth_limit(depth) .set_complexity_limit(complexity) From 2e2d6ca0aeea64cb90d394fdeb145f3713b6e169 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 17:36:39 +0100 Subject: [PATCH 29/85] update sqlite example --- examples/sqlite/src/entities/film.rs | 1 + examples/sqlite/src/entities/film_text.rs | 1 + examples/sqlite/src/entities/language.rs | 3 +-- examples/sqlite/src/entities/mod.rs | 19 +++++++++++++++ examples/sqlite/src/entities/staff.rs | 2 +- examples/sqlite/src/main.rs | 26 +++++++++++++++----- examples/sqlite/src/query_root.rs | 29 ++++------------------- 7 files changed, 48 insertions(+), 33 deletions(-) diff --git a/examples/sqlite/src/entities/film.rs b/examples/sqlite/src/entities/film.rs index 9a5e7832..b2521501 100644 --- a/examples/sqlite/src/entities/film.rs +++ b/examples/sqlite/src/entities/film.rs @@ -6,6 +6,7 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub film_id: i32, pub title: String, + #[sea_orm(column_type = "Text", nullable)] pub description: Option, pub release_year: Option, pub language_id: i16, diff --git a/examples/sqlite/src/entities/film_text.rs b/examples/sqlite/src/entities/film_text.rs index 00bbfab9..2a09460f 100644 --- a/examples/sqlite/src/entities/film_text.rs +++ b/examples/sqlite/src/entities/film_text.rs @@ -6,6 +6,7 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub film_id: i16, pub title: String, + #[sea_orm(column_type = "Text", nullable)] pub description: Option, } diff --git a/examples/sqlite/src/entities/language.rs b/examples/sqlite/src/entities/language.rs index 650001e3..0e906e9b 100644 --- a/examples/sqlite/src/entities/language.rs +++ b/examples/sqlite/src/entities/language.rs @@ -5,8 +5,7 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub language_id: i16, - #[sea_orm(column_type = "Binary(255)")] - pub name: Vec, + pub name: String, pub last_update: DateTimeUtc, } diff --git a/examples/sqlite/src/entities/mod.rs b/examples/sqlite/src/entities/mod.rs index 8fbead53..59213d1d 100644 --- a/examples/sqlite/src/entities/mod.rs +++ b/examples/sqlite/src/entities/mod.rs @@ -16,3 +16,22 @@ pub mod payment; pub mod rental; pub mod staff; pub mod store; + +seaography::register_entity_modules!([ + actor, + address, + category, + city, + country, + customer, + film, + film_actor, + film_category, + film_text, + inventory, + language, + payment, + rental, + staff, + store, +]); diff --git a/examples/sqlite/src/entities/staff.rs b/examples/sqlite/src/entities/staff.rs index bdf6faf3..781654eb 100644 --- a/examples/sqlite/src/entities/staff.rs +++ b/examples/sqlite/src/entities/staff.rs @@ -9,7 +9,7 @@ pub struct Model { pub last_name: String, pub address_id: i32, pub reports_to_id: Option, - #[sea_orm(column_type = "Binary(255)", nullable)] + #[sea_orm(column_type = "Blob", nullable)] pub picture: Option>, pub email: Option, pub store_id: i32, diff --git a/examples/sqlite/src/main.rs b/examples/sqlite/src/main.rs index 37ea9085..910681a5 100644 --- a/examples/sqlite/src/main.rs +++ b/examples/sqlite/src/main.rs @@ -1,12 +1,20 @@ -use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; -use async_graphql_poem::GraphQL; +use async_graphql::{ + dynamic::Schema, + http::{playground_source, GraphQLPlaygroundConfig}, +}; +use async_graphql_poem::{GraphQLRequest, GraphQLResponse}; use dotenv::dotenv; -use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; +use poem::{ + get, handler, + listener::TcpListener, + web::{Data, Html}, + EndpointExt, IntoResponse, Route, Server, +}; use sea_orm::Database; -use seaography::{async_graphql, lazy_static}; +use seaography::{async_graphql, lazy_static::lazy_static}; use std::env; -lazy_static::lazy_static! { +lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); static ref DATABASE_URL: String = @@ -25,6 +33,12 @@ async fn graphql_playground() -> impl IntoResponse { Html(playground_source(GraphQLPlaygroundConfig::new(&ENDPOINT))) } +#[handler] +async fn graphql_handler(schema: Data<&Schema>, req: GraphQLRequest) -> GraphQLResponse { + let req = req.0; + schema.execute(req).await.into() +} + #[tokio::main] async fn main() { dotenv().ok(); @@ -40,7 +54,7 @@ async fn main() { .unwrap(); let app = Route::new().at( &*ENDPOINT, - get(graphql_playground).post(GraphQL::new(schema)), + get(graphql_playground).post(graphql_handler).data(schema), ); println!("Visit GraphQL Playground at http://{}", *URL); Server::new(TcpListener::bind(&*URL)) diff --git a/examples/sqlite/src/query_root.rs b/examples/sqlite/src/query_root.rs index fc6b9cb6..715e991a 100644 --- a/examples/sqlite/src/query_root.rs +++ b/examples/sqlite/src/query_root.rs @@ -1,13 +1,15 @@ use crate::entities::*; use async_graphql::dynamic::*; use sea_orm::DatabaseConnection; -use seaography::{async_graphql, lazy_static, Builder, BuilderContext}; +use seaography::{async_graphql, lazy_static::lazy_static, Builder, BuilderContext}; mod mutations; mod queries; mod types; -lazy_static::lazy_static! { static ref CONTEXT : BuilderContext = BuilderContext :: default () ; } +lazy_static! { + static ref CONTEXT: BuilderContext = BuilderContext::default(); +} pub fn schema( database: DatabaseConnection, @@ -24,28 +26,7 @@ pub fn schema_builder( complexity: Option, ) -> SchemaBuilder { let mut builder = Builder::new(context, database.clone()); - - seaography::register_entities!( - builder, - [ - actor, - address, - category, - city, - country, - customer, - film, - film_actor, - film_category, - film_text, - inventory, - language, - payment, - rental, - staff, - store, - ] - ); + builder = register_entity_modules(builder); // // if `strict-custom-types` is enabled, add the following: // seaography::impl_custom_output_type_for_entities!([actor, ..]); From 6c059a3679e9643c04a3eb9d777baf03c7d076c4 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 18:19:32 +0100 Subject: [PATCH 30/85] Update seaography-cli --- .github/workflows/tests.yaml | 19 ++++++++++++------- README.md | 6 +++--- cli/src/main.rs | 14 +++++++++----- src/lib.rs | 6 +++--- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8a975da6..acdefa16 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -80,8 +80,10 @@ jobs: with: command: run args: > - --package seaography-cli -- ./examples/sqlite - ./examples/sqlite/src/entities sqlite://sakila.db + --package seaography-cli -- + -o ./examples/sqlite + -e ./examples/sqlite/src/entities + -u sqlite://sakila.db seaography-sqlite-example -f poem - name: Depends on local seaography run: >- @@ -144,8 +146,10 @@ jobs: with: command: run args: > - --package seaography-cli -- ./examples/mysql - ./examples/mysql/src/entities mysql://sea:sea@127.0.0.1/sakila + --package seaography-cli -- + -o ./examples/mysql + -e ./examples/mysql/src/entities + -u mysql://sea:sea@127.0.0.1/sakila seaography-mysql-example -f actix - name: Depends on local seaography run: >- @@ -213,9 +217,10 @@ jobs: with: command: run args: > - --package seaography-cli -- ./examples/postgres - ./examples/postgres/src/entities - postgres://sea:sea@127.0.0.1/sakila?currentSchema=public + --package seaography-cli -- + -o ./examples/postgres + -e ./examples/postgres/src/entities + -u postgres://sea:sea@127.0.0.1/sakila?currentSchema=public seaography-postgres-example -f axum - name: Depends on local seaography run: >- diff --git a/README.md b/README.md index 648d194d..5081d5d8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Then regenerate example project like below, or simply do `cargo run`. ```sh cd examples/mysql sea-orm-cli generate entity -o src/entities -u mysql://user:pw@127.0.0.1/sakila --seaography -seaography-cli ./ src/entities mysql://user:pw@127.0.0.1/sakila seaography-mysql-example +seaography-cli -o ./ -e src/entities -u mysql://user:pw@127.0.0.1/sakila seaography-mysql-example cargo run ``` @@ -88,7 +88,7 @@ Then regenerate example project like below, or simply do `cargo run`. ```sh cd examples/postgres sea-orm-cli generate entity -o src/entities -u postgres://user:pw@localhost/sakila --seaography -seaography-cli ./ src/entities postgres://user:pw@localhost/sakila seaography-postgres-example +seaography-cli -o ./ -e src/entities -u postgres://user:pw@localhost/sakila seaography-postgres-example cargo run ``` @@ -99,7 +99,7 @@ cargo run ```sh cd examples/sqlite sea-orm-cli generate entity -o src/entities -u sqlite://sakila.db --seaography -seaography-cli ./ src/entities sqlite://sakila.db seaography-sqlite-example +seaography-cli -o ./ -e src/entities -u sqlite://sakila.db seaography-sqlite-example cargo run ``` diff --git a/cli/src/main.rs b/cli/src/main.rs index 3cd7068d..e7f9ce76 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,13 +4,17 @@ use seaography_generator::write_project; #[derive(clap::Parser)] #[clap(author, version, about, long_about = None)] pub struct Args { - /// Project destination folder - pub destination: String, + #[arg( + short = 'o', + long, + default_value = "./", + help = "Project output directory" + )] + pub output_dir: String, - /// SeaORM entities folder + #[arg(short = 'e', long, help = "Entities directory")] pub entities: String, - /// Database URL #[arg( short = 'u', long, @@ -94,7 +98,7 @@ pub fn parse_database_url(database_url: &str) -> Result Date: Sun, 19 Oct 2025 18:39:41 +0100 Subject: [PATCH 31/85] Fix CI --- .github/workflows/tests.yaml | 6 +++--- cli/src/main.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index acdefa16..90d820bb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -57,7 +57,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 1.0.0 + version: 2.0.0-rc.12 - name: Additional Integration tests working-directory: ./examples/sqlite run: | @@ -122,7 +122,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 1.0.0 + version: 2.0.0-rc.12 - name: Remove generated folder run: rm -rf ./examples/mysql/src - name: Create DB @@ -187,7 +187,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 1.0.0 + version: 2.0.0-rc.12 - name: Create DB run: >- psql -q postgres://sea:sea@localhost/postgres -c 'CREATE DATABASE diff --git a/cli/src/main.rs b/cli/src/main.rs index e7f9ce76..e0d804c4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -109,8 +109,8 @@ async fn main() { let db_url = database_url.as_str(); write_project( - &root_path, - &entities_path, + root_path, + entities_path, db_url, &args.crate_name, sql_library, From d0e9d8982c78235a2f3a540c778ecceb4e969312 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 19:03:01 +0100 Subject: [PATCH 32/85] Update CLI --- .github/workflows/tests.yaml | 6 +++--- Cargo.toml | 2 +- README.md | 2 +- cli/Cargo.toml | 2 +- generator/Cargo.toml | 2 +- src/lib.rs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 90d820bb..92ada550 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -57,7 +57,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 2.0.0-rc.12 + version: 2.0.0-rc.13 - name: Additional Integration tests working-directory: ./examples/sqlite run: | @@ -122,7 +122,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 2.0.0-rc.12 + version: 2.0.0-rc.13 - name: Remove generated folder run: rm -rf ./examples/mysql/src - name: Create DB @@ -187,7 +187,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 2.0.0-rc.12 + version: 2.0.0-rc.13 - name: Create DB run: >- psql -q postgres://sea:sea@localhost/postgres -c 'CREATE DATABASE diff --git a/Cargo.toml b/Cargo.toml index 91b31e8e..cd26c093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ authors = [ "Panagiotis Karatakis ", "Chris Tsang ", ] -description = "🧭 A dynamic GraphQL framework for SeaORM" +description = "🧭 A GraphQL framework for SeaORM" license = "MIT OR Apache-2.0" homepage = "https://www.sea-ql.org/Seaography" documentation = "https://docs.rs/seaography" diff --git a/README.md b/README.md index 5081d5d8..a71e9e15 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Seaography logo

- 🧭 A dynamic GraphQL framework for SeaORM + 🧭 A GraphQL framework for SeaORM

[![crate](https://img.shields.io/crates/v/seaography.svg)](https://crates.io/crates/seaography) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8f8aa8ae..5e8aaa23 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0-rc.2" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] -description = "🧭 A dynamic GraphQL framework for SeaORM" +description = "🧭 A GraphQL framework for SeaORM" license = "MIT OR Apache-2.0" homepage = "https://www.sea-ql.org/Seaography" documentation = "https://docs.rs/seaography" diff --git a/generator/Cargo.toml b/generator/Cargo.toml index babe149c..5009e3fb 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0-rc.2" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] -description = "🧭 A dynamic GraphQL framework for SeaORM" +description = "🧭 A GraphQL framework for SeaORM" license = "MIT OR Apache-2.0" homepage = "https://www.sea-ql.org/Seaography" documentation = "https://docs.rs/seaography" diff --git a/src/lib.rs b/src/lib.rs index f29cba61..e20ffe9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ //! Seaography logo //! //!

-//! 🧭 A dynamic GraphQL framework for SeaORM +//! 🧭 A GraphQL framework for SeaORM //!

//! //! [![crate](https://img.shields.io/crates/v/seaography.svg)](https://crates.io/crates/seaography) From 1ceb3cb47c8e233cb1d01087be8f0140a3573e26 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 19:55:40 +0100 Subject: [PATCH 33/85] Update mysql example --- .github/workflows/tests.yaml | 4 ---- examples/mysql/src/entities/film.rs | 7 +++++++ examples/mysql/src/entities/mod.rs | 20 +++++++++++++++++++ examples/mysql/src/main.rs | 4 ++-- examples/mysql/src/query_root.rs | 30 ++++++----------------------- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 92ada550..a2133591 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -155,8 +155,6 @@ jobs: run: >- sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' ./examples/mysql/Cargo.toml - - name: Fix Nullable not implemented for Vec and tsvector - run: 'sed -i "24,28d" ./examples/mysql/src/entities/film.rs' - name: Build example working-directory: ./examples/mysql run: cargo build @@ -226,8 +224,6 @@ jobs: run: >- sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' ./examples/postgres/Cargo.toml - - name: Fix Nullable not implemented for Vec and tsvector - run: 'sed -i "26,27d" ./examples/postgres/src/entities/film.rs' - name: Build example working-directory: ./examples/postgres run: cargo build diff --git a/examples/mysql/src/entities/film.rs b/examples/mysql/src/entities/film.rs index b5e62003..7f5ad689 100644 --- a/examples/mysql/src/entities/film.rs +++ b/examples/mysql/src/entities/film.rs @@ -19,6 +19,13 @@ pub struct Model { #[sea_orm(column_type = "Decimal(Some((5, 2)))")] pub replacement_cost: Decimal, pub rating: Option, + #[sea_orm( + ignore, + column_type = "custom(\"SET ('Trailers', 'Commentaries', 'Deleted Scenes', 'Behind the Scenes')\")", + select_as = "text", + nullable + )] + pub special_features: Option, pub last_update: DateTimeUtc, } diff --git a/examples/mysql/src/entities/mod.rs b/examples/mysql/src/entities/mod.rs index fde3e1fc..1a0f9460 100644 --- a/examples/mysql/src/entities/mod.rs +++ b/examples/mysql/src/entities/mod.rs @@ -17,3 +17,23 @@ pub mod rental; pub mod sea_orm_active_enums; pub mod staff; pub mod store; + +seaography::register_entity_modules!([ + actor, + address, + category, + city, + country, + customer, + film, + film_actor, + film_category, + film_text, + inventory, + language, + payment, + rental, + staff, + store, +]); +seaography::register_active_enums!([sea_orm_active_enums::Rating,]); diff --git a/examples/mysql/src/main.rs b/examples/mysql/src/main.rs index bc2cba4d..4ffc767b 100644 --- a/examples/mysql/src/main.rs +++ b/examples/mysql/src/main.rs @@ -6,10 +6,10 @@ use async_graphql::{ use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; use dotenv::dotenv; use sea_orm::Database; -use seaography::{async_graphql, lazy_static}; +use seaography::{async_graphql, lazy_static::lazy_static}; use std::env; -lazy_static::lazy_static! { +lazy_static! { static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); static ref DATABASE_URL: String = diff --git a/examples/mysql/src/query_root.rs b/examples/mysql/src/query_root.rs index 0a09407c..6471b0ec 100644 --- a/examples/mysql/src/query_root.rs +++ b/examples/mysql/src/query_root.rs @@ -1,9 +1,11 @@ use crate::entities::*; use async_graphql::dynamic::*; use sea_orm::DatabaseConnection; -use seaography::{async_graphql, lazy_static, Builder, BuilderContext}; +use seaography::{async_graphql, lazy_static::lazy_static, Builder, BuilderContext}; -lazy_static::lazy_static! { static ref CONTEXT : BuilderContext = BuilderContext :: default () ; } +lazy_static! { + static ref CONTEXT: BuilderContext = BuilderContext::default(); +} pub fn schema( database: DatabaseConnection, @@ -20,28 +22,8 @@ pub fn schema_builder( complexity: Option, ) -> SchemaBuilder { let mut builder = Builder::new(context, database.clone()); - seaography::register_entities!( - builder, - [ - actor, - address, - category, - city, - country, - customer, - film, - film_actor, - film_category, - film_text, - inventory, - language, - payment, - rental, - staff, - store, - ] - ); - builder.register_enumeration::(); + builder = register_entity_modules(builder); + builder = register_active_enums(builder); builder .set_depth_limit(depth) .set_complexity_limit(complexity) From 2374c971784b93fb4d7cfc3c3eb273b1de7297ae Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 20:43:04 +0100 Subject: [PATCH 34/85] Use tokio inplace of async std --- examples/mysql/Cargo.toml | 2 +- examples/postgres/Cargo.toml | 2 +- examples/sqlite/Cargo.toml | 2 +- generator/src/templates/actix_cargo.toml | 2 +- generator/src/templates/axum_cargo.toml | 2 +- generator/src/templates/poem_cargo.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index d5392f80..bf53c7be 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -7,7 +7,7 @@ version = "2.0.0-rc.2" actix-web = { version = "4.5", default-features = false, features = ["macros"] } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["sqlx-mysql", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 65eee302..f8360769 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -7,7 +7,7 @@ version = "2.0.0-rc.2" axum = { version = "0.8" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["sqlx-postgres", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 049306a2..1f9ece30 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -7,7 +7,7 @@ version = "2.0.0-rc.2" poem = { version = "3.0" } async-graphql-poem = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["sqlx-sqlite", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index fc3395c3..3f5decc9 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" actix-web = { version = "4.5", default-features = false, features = ["macros"] } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/generator/src/templates/axum_cargo.toml b/generator/src/templates/axum_cargo.toml index 90e93ff9..586702e4 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" axum = { version = "0.8" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } diff --git a/generator/src/templates/poem_cargo.toml b/generator/src/templates/poem_cargo.toml index 7004eaff..2e273fe1 100644 --- a/generator/src/templates/poem_cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" poem = { version = "3.0" } async-graphql-poem = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-async-std-native-tls", "seaography"] } +sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } From ac9efcad5ed0f27eab87ede46b8a768cfe84ca5c Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 19 Oct 2025 23:47:19 +0100 Subject: [PATCH 35/85] One more test case --- examples/sqlite/tests/query_tests.rs | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index be43db64..6e414df1 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -1335,3 +1335,48 @@ async fn country_having_city() { "#, ) } + +#[tokio::test] +async fn film_filter_or() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + film( + filters: { + or: [{ title: { contains: "LIFE" } }, { title: { contains: "DOG" } }] + } + ) { + nodes { + title + } + } + } + "#, + ) + .await, + r#" + { + "film": { + "nodes": [ + { + "title": "ANGELS LIFE" + }, + { + "title": "ARABIA DOGMA" + }, + { + "title": "DOGMA FAMILY" + }, + { + "title": "LIFE TWISTED" + } + ] + } + } + "#, + ) +} From 16d13d60af6883ce9fc7a4246b68f6d1c6df1266 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 21 Oct 2025 11:52:00 +0100 Subject: [PATCH 36/85] Make mutation tests repeatable --- .github/workflows/tests.yaml | 9 +- examples/postgres/Cargo.toml | 1 + .../postgres/tests/entity_metadata_tests.rs | 4 +- examples/postgres/tests/mutation_tests.rs | 512 +++++++++++------- 4 files changed, 339 insertions(+), 187 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a2133591..fb99c61f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -202,7 +202,8 @@ jobs: - name: Additional Integration tests working-directory: ./examples/postgres run: | - cargo test --test pg_query_tests entity_metadata_tests + cargo test --test pg_query_tests + cargo test --test entity_metadata_tests rm tests/pg_query_tests.rs tests/entity_metadata_tests.rs - name: Remove generated folder run: rm -rf ./examples/postgres/src @@ -221,9 +222,9 @@ jobs: -u postgres://sea:sea@127.0.0.1/sakila?currentSchema=public seaography-postgres-example -f axum - name: Depends on local seaography - run: >- - sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' - ./examples/postgres/Cargo.toml + run: | + sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' ./examples/postgres/Cargo.toml + sed -i '/^\[dev.dependencies\]$/a \serde = { version = "1", features = ["derive"] }' ./examples/postgres/Cargo.toml - name: Build example working-directory: ./examples/postgres run: cargo build diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index f8360769..7d3186a7 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -18,6 +18,7 @@ version = "~2.0.0-rc.2" # seaography version features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] +serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.103" } [workspace] diff --git a/examples/postgres/tests/entity_metadata_tests.rs b/examples/postgres/tests/entity_metadata_tests.rs index 40c22c40..bd5df445 100644 --- a/examples/postgres/tests/entity_metadata_tests.rs +++ b/examples/postgres/tests/entity_metadata_tests.rs @@ -215,10 +215,10 @@ async fn test_entity_metadata() { "name": "mpaa_rating", "variants": [ "G", - "NC-17", "PG", "PG-13", - "R" + "R", + "NC-17" ] } } diff --git a/examples/postgres/tests/mutation_tests.rs b/examples/postgres/tests/mutation_tests.rs index 475ee49e..b754ead0 100644 --- a/examples/postgres/tests/mutation_tests.rs +++ b/examples/postgres/tests/mutation_tests.rs @@ -1,9 +1,15 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; use seaography::async_graphql; +use serde::Deserialize; #[tokio::test] async fn main() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .init(); + test_simple_insert_one().await; test_complex_insert_one().await; test_create_batch_mutation().await; @@ -28,6 +34,16 @@ fn assert_eq(a: Response, b: &str) { async fn test_simple_insert_one() { let schema = schema().await; + schema + .execute( + r#" + mutation { + filmActorDelete(filter: { lastUpdate: { gt: "2022-11-14 10:30:12" } }) + } + "#, + ) + .await; + assert_eq( schema .execute( @@ -110,12 +126,43 @@ async fn test_simple_insert_one() { async fn test_complex_insert_one() { let schema = schema().await; - assert_eq( - schema - .execute( - r#" + schema + .execute( + r#" + mutation { + rentalDelete( + filter: { + rentalDate: { eq: "2030-01-25 21:50:05" } + inventoryId: { eq: 4452 } + customerId: { eq: 319 } + } + ) + } + "#, + ) + .await; + + #[derive(Deserialize)] + struct QueryResult { + rental: RentalQuery, + } + + #[derive(Deserialize)] + struct RentalQuery { + nodes: Vec, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct RentalId { + rental_id: i32, + } + + let response = schema + .execute( + r#" { - rental(filters: { rentalId: { eq: 16050 } }) { + rental(orderBy: { rentalId: DESC }) { nodes { rentalId inventoryId @@ -126,141 +173,176 @@ async fn test_complex_insert_one() { } } "#, - ) - .await, - r#" - { - "rental": { - "nodes": [ - ] - } - } - "#, - ); + ) + .await + .data + .into_json() + .unwrap(); - assert_eq( - schema - .execute( - r#" + let result: QueryResult = serde_json::from_value(response).unwrap(); + let max_id = result + .rental + .nodes + .iter() + .map(|r| r.rental_id) + .max() + .unwrap_or_default(); + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct CreateResult { + rental_create_one: RentalObject, + } + + #[derive(Debug, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + struct RentalObject { + rental_id: i32, + inventory_id: i32, + customer_id: i32, + staff_id: i32, + return_date: String, + } + + let response = schema + .execute( + r#" mutation { - rentalCreateOne( - data: { - rentalId: 16050 - rentalDate: "2030-01-25 21:50:05" - inventoryId: 4452 - customerId: 319 - returnDate: "2030-01-12 21:50:05" - staffId: 1 - lastUpdate: "2030-01-01 21:50:05" - } - ) { - rentalId - inventoryId - customerId - returnDate - staffId + rentalCreateOne( + data: { + rentalDate: "2030-01-25 21:50:05" + inventoryId: 4452 + customerId: 319 + returnDate: "2030-01-12 21:50:05" + staffId: 1 + lastUpdate: "2030-01-01 21:50:05" } + ) { + rentalId + inventoryId + customerId + returnDate + staffId + } } "#, - ) - .await, - r#" - { - "rentalCreateOne": { - "rentalId": 16050, - "inventoryId": 4452, - "customerId": 319, - "returnDate": "2030-01-12 21:50:05", - "staffId": 1 - } - } - "#, + ) + .await + .data + .into_json() + .unwrap(); + + let result: CreateResult = serde_json::from_value(response).unwrap(); + let rental = result.rental_create_one; + let rental_id = rental.rental_id; + assert!(rental.rental_id > max_id); + assert_eq!( + rental, + RentalObject { + rental_id, + inventory_id: 4452, + customer_id: 319, + staff_id: 1, + return_date: "2030-01-12 21:50:05".into(), + } ); assert_eq( schema - .execute( + .execute(format!( r#" - { - rental(filters: { rentalId: { eq: 16050 } }) { - nodes { - rentalId - inventoryId - customerId - returnDate - staffId - } - } - } - "#, - ) + {{ + rental(filters: {{ rentalId: {{ eq: {rental_id} }} }}) {{ + nodes {{ + rentalId + inventoryId + customerId + staffId + returnDate + }} + }} + }} + "# + )) .await, - r#" - { - "rental": { - "nodes": [ - { - "rentalId": 16050, - "inventoryId": 4452, - "customerId": 319, - "returnDate": "2030-01-12 21:50:05", - "staffId": 1 - } - ] - } - } - "#, + &format!( + r#" + {{ + "rental": {{ + "nodes": [ + {{ + "rentalId": {rental_id}, + "inventoryId": 4452, + "customerId": 319, + "staffId": 1, + "returnDate": "2030-01-12 21:50:05" + }} + ] + }} + }} + "# + ), ); } async fn test_create_batch_mutation() { let schema = schema().await; + schema + .execute( + r#" + mutation { + languageDelete(filter: { languageId: { gt: 6 } }) + } + "#, + ) + .await; + assert_eq( schema .execute( r#" { - language(filters: { languageId: { lte: 8 } }, orderBy: { languageId: ASC }) { - nodes { - languageId - name - } + language(orderBy: { languageId: ASC }) { + nodes { + languageId + name } + } } "#, ) .await, r#" { - "language": { - "nodes": [ - { - "languageId": 1, - "name": "English " - }, - { - "languageId": 2, - "name": "Italian " - }, - { - "languageId": 3, - "name": "Japanese " - }, - { - "languageId": 4, - "name": "Mandarin " - }, - { - "languageId": 5, - "name": "French " - }, - { - "languageId": 6, - "name": "German " - } - ] - } + "language": { + "nodes": [ + { + "languageId": 1, + "name": "English " + }, + { + "languageId": 2, + "name": "Italian " + }, + { + "languageId": 3, + "name": "Japanese " + }, + { + "languageId": 4, + "name": "Mandarin " + }, + { + "languageId": 5, + "name": "French " + }, + { + "languageId": 6, + "name": "German " + } + ] + } } "#, ); @@ -270,28 +352,28 @@ async fn test_create_batch_mutation() { .execute( r#" mutation { - languageCreateBatch( - data: [ - { languageId: 1, name: "Swedish", lastUpdate: "2030-01-12 21:50:05" } - { languageId: 1, name: "Danish", lastUpdate: "2030-01-12 21:50:05" } - ] - ) { - name - } + languageCreateBatch( + data: [ + { name: "Swedish", lastUpdate: "2030-01-12 21:50:05" } + { name: "Danish", lastUpdate: "2030-01-12 21:50:05" } + ] + ) { + name + } } "#, ) .await, r#" { - "languageCreateBatch": [ + "languageCreateBatch": [ { - "name": "Swedish " + "name": "Swedish " }, { - "name": "Danish " + "name": "Danish " } - ] + ] } "#, ); @@ -301,11 +383,11 @@ async fn test_create_batch_mutation() { .execute( r#" { - language(filters: { languageId: { lte: 8 } }, orderBy: { languageId: ASC }) { - nodes { - name - } + language(orderBy: { languageId: ASC }) { + nodes { + name } + } } "#, ) @@ -343,7 +425,42 @@ async fn test_create_batch_mutation() { } } "#, - ) + ); + + schema + .execute( + r#" + mutation { + languageDelete(filter: { name: { is_in: ["Swedish", "Danish"] } }) + } + "#, + ) + .await; + + assert_eq( + schema + .execute( + r#" + { + language(filters: { languageId: { gt: 6 } }) { + nodes { + languageId + name + } + } + } + "#, + ) + .await, + r#" + { + "language": { + "nodes": [ + ] + } + } + "#, + ); } async fn test_update_mutation() { @@ -354,12 +471,12 @@ async fn test_update_mutation() { .execute( r#" { - country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { - nodes { - country - countryId - } + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId } + } } "#, ) @@ -403,13 +520,13 @@ async fn test_update_mutation() { .execute( r#" mutation { - countryUpdate( - data: { country: "[DELETED]" } - filter: { countryId: { lt: 6 } } - ) { - countryId - country - } + countryUpdate( + data: { country: "[DELETED]" } + filter: { countryId: { lt: 6 } } + ) { + countryId + country + } } "#, ) @@ -447,12 +564,12 @@ async fn test_update_mutation() { .execute( r#" { - country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { - nodes { - country - countryId - } + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId } + } } "#, ) @@ -490,6 +607,42 @@ async fn test_update_mutation() { } "#, ); + + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Afghanistan" } filter: { countryId: { eq: 1 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Algeria" } filter: { countryId: { eq: 2 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "American Samoa" } filter: { countryId: { eq: 3 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Angola" } filter: { countryId: { eq: 4 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Anguilla" } filter: { countryId: { eq: 5 } }) { country } + }"#, + ) + .await; } async fn test_delete_mutation() { @@ -500,11 +653,11 @@ async fn test_delete_mutation() { .execute( r#" { - language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { - nodes { - languageId - } + language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { + nodes { + languageId } + } } "#, ) @@ -523,15 +676,15 @@ async fn test_delete_mutation() { .execute( r#" mutation { - languageCreateBatch( - data: [ - { languageId: 9, name: "9", lastUpdate: "2030-01-12 21:50:05" } - { languageId: 10, name: "10", lastUpdate: "2030-01-12 21:50:05" } - { languageId: 11, name: "11", lastUpdate: "2030-01-12 21:50:05" } - ] - ) { - languageId - } + languageCreateBatch( + data: [ + { name: "9", lastUpdate: "2030-01-12 21:50:05" } + { name: "10", lastUpdate: "2030-01-12 21:50:05" } + { name: "11", lastUpdate: "2030-01-12 21:50:05" } + ] + ) { + name + } } "#, ) @@ -540,13 +693,13 @@ async fn test_delete_mutation() { { "languageCreateBatch": [ { - "languageId": 9 + "name": "9 " }, { - "languageId": 10 + "name": "10 " }, { - "languageId": 11 + "name": "11 " } ] } @@ -558,11 +711,11 @@ async fn test_delete_mutation() { .execute( r#" { - language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { - nodes { - languageId - } + language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { + nodes { + name } + } } "#, ) @@ -572,13 +725,13 @@ async fn test_delete_mutation() { "language": { "nodes": [ { - "languageId": 9 + "name": "9 " }, { - "languageId": 10 + "name": "10 " }, { - "languageId": 11 + "name": "11 " } ] } @@ -591,14 +744,14 @@ async fn test_delete_mutation() { .execute( r#" mutation { - languageDelete(filter: { languageId: { gte: 10 } }) + languageDelete(filter: { languageId: { gt: 6 } }) } "#, ) .await, r#" { - "languageDelete": 2 + "languageDelete": 3 } "#, ); @@ -608,24 +761,21 @@ async fn test_delete_mutation() { .execute( r#" { - language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { - nodes { - languageId - } + language(filters: { languageId: { gt: 6 } }, orderBy: { languageId: ASC }) { + nodes { + languageId } + } } "#, ) .await, r#" { - "language": { - "nodes": [ - { - "languageId": 9 - } - ] - } + "language": { + "nodes": [ + ] + } } "#, ); From a8b403b3e100b52576084d4fd1f25bb6101cfa3c Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 22 Oct 2025 16:31:07 +0100 Subject: [PATCH 37/85] 2.0.0-rc.3 --- Cargo.toml | 4 ++-- cli/Cargo.toml | 4 ++-- generator/Cargo.toml | 2 +- macros/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cd26c093..9b70876a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" edition = "2021" rust-version = "1.70" authors = [ @@ -25,7 +25,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } -seaography-macros = { version = "~2.0.0-rc.2", path = "macros", optional = true } +seaography-macros = { version = "~2.0.0-rc.3", path = "macros", optional = true } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5e8aaa23..3bb57d7f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-cli" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } clap = { version = "4.3.19", features = ["env", "derive"] } -seaography-generator = { version = "~2.0.0-rc.2", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc.3", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/generator/Cargo.toml b/generator/Cargo.toml index 5009e3fb..88d4069c 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-generator" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 2f086ba6..6c54982d 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-macros" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" edition = "2024" rust-version = "1.85" authors = ["Chris Tsang "] From 221b15c7211063831320099d8773ea5a1f6da9a8 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 22 Oct 2025 16:31:07 +0100 Subject: [PATCH 38/85] update examples --- examples/mysql/Cargo.toml | 4 ++-- examples/postgres/Cargo.toml | 4 ++-- examples/sea-draw/Cargo.toml | 4 ++-- examples/sqlite/Cargo.toml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index bf53c7be..5d4e2a11 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-mysql-example" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" [dependencies] actix-web = { version = "4.5", default-features = false, features = ["macros"] } @@ -14,7 +14,7 @@ tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.2" # seaography version +version = "~2.0.0-rc.3" # seaography version features = ["with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 7d3186a7..4647f482 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" [dependencies] axum = { version = "0.8" } @@ -14,7 +14,7 @@ tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.2" # seaography version +version = "~2.0.0-rc.3" # seaography version features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index f88c6c7e..8ae58a9b 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-draw" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" edition = "2024" [dependencies] @@ -30,7 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.2" # seaography version +version = "~2.0.0-rc.3" # seaography version features = [ "macros", "field-pluralize", diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 1f9ece30..b665c23a 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "2.0.0-rc.2" +version = "2.0.0-rc.3" [dependencies] poem = { version = "3.0" } @@ -13,7 +13,7 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.seaography] -version = "~2.0.0-rc.2" # seaography version +version = "~2.0.0-rc.3" # seaography version features = ["with-decimal", "with-chrono"] [dev-dependencies] From 453e7916f471c599e4d196eabeff34859cf83704 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 22 Oct 2025 16:32:29 +0100 Subject: [PATCH 39/85] rename scripts --- build-tools/{bump-version.sh => bump.sh} | 0 build-tools/{cargo-publish.sh => publish.sh} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename build-tools/{bump-version.sh => bump.sh} (100%) rename build-tools/{cargo-publish.sh => publish.sh} (100%) diff --git a/build-tools/bump-version.sh b/build-tools/bump.sh similarity index 100% rename from build-tools/bump-version.sh rename to build-tools/bump.sh diff --git a/build-tools/cargo-publish.sh b/build-tools/publish.sh similarity index 100% rename from build-tools/cargo-publish.sh rename to build-tools/publish.sh From 270bd8a0f167e60433ddfbf9e14d1cd621ccca77 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 22 Oct 2025 18:23:44 +0100 Subject: [PATCH 40/85] Update cargo template --- examples/mysql/Cargo.toml | 5 ++++- examples/postgres/Cargo.toml | 5 ++++- examples/sqlite/Cargo.toml | 5 ++++- generator/src/templates/actix_cargo.toml | 5 ++++- generator/src/templates/axum_cargo.toml | 5 ++++- generator/src/templates/poem_cargo.toml | 5 ++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index 5d4e2a11..acb2fbd8 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -7,11 +7,14 @@ version = "2.0.0-rc.3" actix-web = { version = "4.5", default-features = false, features = ["macros"] } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] + [dependencies.seaography] path = "../../" version = "~2.0.0-rc.3" # seaography version diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 4647f482..955d90f7 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -7,11 +7,14 @@ version = "2.0.0-rc.3" axum = { version = "0.8" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] + [dependencies.seaography] path = "../../" version = "~2.0.0-rc.3" # seaography version diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index b665c23a..1e219766 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -7,11 +7,14 @@ version = "2.0.0-rc.3" poem = { version = "3.0" } async-graphql-poem = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] + [dependencies.seaography] version = "~2.0.0-rc.3" # seaography version features = ["with-decimal", "with-chrono"] diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index 3f5decc9..69e02d5c 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -7,11 +7,14 @@ version = "0.1.0" actix-web = { version = "4.5", default-features = false, features = ["macros"] } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +features = ["", "runtime-tokio-native-tls", "seaography"] + [dependencies.seaography] version = "~" # seaography version features = ["with-decimal", "with-chrono"] diff --git a/generator/src/templates/axum_cargo.toml b/generator/src/templates/axum_cargo.toml index 586702e4..cd1e989d 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -7,11 +7,14 @@ version = "0.1.0" axum = { version = "0.8" } async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +features = ["", "runtime-tokio-native-tls", "seaography"] + [dependencies.seaography] version = "~" # seaography version features = ["with-decimal", "with-chrono"] diff --git a/generator/src/templates/poem_cargo.toml b/generator/src/templates/poem_cargo.toml index 2e273fe1..0414b360 100644 --- a/generator/src/templates/poem_cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -7,11 +7,14 @@ version = "0.1.0" poem = { version = "3.0" } async-graphql-poem = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~2.0.0-rc", features = ["", "runtime-tokio-native-tls", "seaography"] } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +features = ["", "runtime-tokio-native-tls", "seaography"] + [dependencies.seaography] version = "~" # seaography version features = ["with-decimal", "with-chrono"] From 2dc14d740f7b88808df625ab36a55c6faaede2cb Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 23 Oct 2025 16:52:02 +0100 Subject: [PATCH 41/85] Optimize EntityColumnId --- src/builder_context/entity_column_id.rs | 17 ++++++++---- src/builder_context/filter_types_map.rs | 37 ++++++++----------------- src/error.rs | 2 ++ 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/builder_context/entity_column_id.rs b/src/builder_context/entity_column_id.rs index 6d348655..56b7215e 100644 --- a/src/builder_context/entity_column_id.rs +++ b/src/builder_context/entity_column_id.rs @@ -1,11 +1,12 @@ use sea_orm::{EntityName, EntityTrait, IdenStatic}; +use std::borrow::Cow; use crate::BuilderContext; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct EntityColumnId { - entity_name: String, - column_name: String, + entity_name: Cow<'static, str>, + column_name: Cow<'static, str>, } impl EntityColumnId { @@ -14,15 +15,15 @@ impl EntityColumnId { T: EntityTrait, { EntityColumnId { - entity_name: ::table_name(&T::default()).into(), - column_name: column.as_str().into(), + entity_name: Cow::Borrowed(::table_name(&T::default())), + column_name: Cow::Borrowed(column.as_str()), } } pub fn with_array(&self) -> Self { Self { entity_name: self.entity_name.clone(), - column_name: format!("{}.array", self.column_name), + column_name: Cow::Owned(format!("{}.array", self.column_name)), } } @@ -35,3 +36,9 @@ impl EntityColumnId { context.entity_object.column_name.as_ref()(&entity_name, &self.column_name) } } + +impl std::fmt::Display for EntityColumnId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}.{}", self.entity_name, self.column_name) + } +} diff --git a/src/builder_context/filter_types_map.rs b/src/builder_context/filter_types_map.rs index 63ef7391..f40e63ea 100644 --- a/src/builder_context/filter_types_map.rs +++ b/src/builder_context/filter_types_map.rs @@ -4,8 +4,8 @@ use async_graphql::dynamic::{InputObject, InputValue, ObjectAccessor, TypeRef}; use sea_orm::{ColumnTrait, ColumnType, Condition, EntityTrait, ExprTrait}; use crate::{ - prepare_enumeration_condition, ActiveEnumFilterInputBuilder, BuilderContext, - EntityObjectBuilder, SeaResult, TypesMapConfig, TypesMapHelper, + prepare_enumeration_condition, ActiveEnumFilterInputBuilder, BuilderContext, EntityColumnId, + EntityObjectBuilder, SeaResult, SeaographyError, TypesMapConfig, TypesMapHelper, }; type FnFilterCondition = @@ -14,9 +14,9 @@ type FnFilterCondition = /// The configuration for FilterTypesMapHelper pub struct FilterTypesMapConfig { /// used to map entity_name.column_name to a custom filter type - pub overwrites: BTreeMap>, + pub overwrites: BTreeMap>, /// used to map entity_name.column_name to a custom condition function - pub condition_functions: BTreeMap, + pub condition_functions: BTreeMap, // basic filters pub string_filter_info: FilterInfo, @@ -235,20 +235,9 @@ impl FilterTypesMapHelper { where T: EntityTrait, { - let entity_object_builder = EntityObjectBuilder { - context: self.context, - }; - - let entity_name = entity_object_builder.type_name::(); - let column_name = entity_object_builder.column_name::(column); + let entity_column_id = EntityColumnId::of::(column); - // used to honor overwrites - if let Some(ty) = self - .context - .filter_types - .overwrites - .get(&format!("{entity_name}.{column_name}")) - { + if let Some(ty) = self.context.filter_types.overwrites.get(&entity_column_id) { return ty.clone(); } @@ -572,23 +561,19 @@ impl FilterTypesMapHelper { return prepare_enumeration_condition::(filter, column, condition) } FilterType::Custom(_) => { - let entity_object_builder = EntityObjectBuilder { - context: self.context, - }; - - let entity_name = entity_object_builder.type_name::(); - let column_name = entity_object_builder.column_name::(column); + let entity_column_id = EntityColumnId::of::(column); if let Some(filter_condition_fn) = self .context .filter_types .condition_functions - .get(&format!("{entity_name}.{column_name}")) + .get(&entity_column_id) { return filter_condition_fn(condition, filter); } else { - // FIXME: add log warning to console - return Ok(condition); + return Err(SeaographyError::CustomFilterError( + entity_column_id.to_string(), + )); } } FilterType::Array(Some(filter_type)) => match *filter_type { diff --git a/src/error.rs b/src/error.rs index 92aa447b..94e7c05d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,8 @@ pub enum SeaographyError { TypeConversionError(String, String), #[error("[array conversion] postgres array can not be nested type of array")] NestedArrayConversionError, + #[error("[custom filter] {0}")] + CustomFilterError(String), #[error("[async_graphql] {0:?}")] UploadError(async_graphql::InputValueError), } From 45cfa8bf96f50d51381a31a3bbc9303acc9d3386 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 23 Oct 2025 18:21:51 +0100 Subject: [PATCH 42/85] Readme --- README.md | 27 ++++++++++++++------------- src/lib.rs | 27 ++++++++++++++------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a71e9e15..5e0b95e3 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ Seaography logo -

- 🧭 A GraphQL framework for SeaORM -

+

🧭 A GraphQL framework for Rust

+

Quick to start, type‑safe, powerful and extensible

[![crate](https://img.shields.io/crates/v/seaography.svg)](https://crates.io/crates/seaography) [![docs](https://docs.rs/seaography/badge.svg)](https://docs.rs/seaography) @@ -14,16 +13,17 @@ # Seaography -Seaography is a GraphQL framework that bridges async-graphql and SeaORM, instantly turning your database into a fully functional GraphQL API in Rust. -It leverages async‑graphql's dynamic schema capabilities, resulting in minimal generated code and faster compile times compared to static schemas. -With extensive configuration options, you can easily tailor the generated GraphQL schema to your application's needs. +Seaography is a **GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), +turning your database schema into a fully-typed GraphQL API with minimal effort. +By leveraging async-graphql's dynamic schema engine, Seaography avoids the heavy code generation of static approaches, resulting in faster compile times. +The generated schema stays in sync with your SeaORM entities, while still giving you full control to extend and customize it. -Seaography enables you to focus on your application logic instead of boilerplate. -With Seaography, you can: +With Seaography you can focus on application logic instead of boilerplate. It enables you to: -+ Turn a set of SeaORM entities into a complete GraphQL schema -+ Use derive macros to craft custom input / output objects, queries and mutations, mix-and-match them with SeaORM models -+ Generate web servers with the included CLI - ready to compile and run ++ Expose a complete GraphQL schema directly from your SeaORM entities, including filters, pagination, and nested relations ++ Use derive macros to define custom input/output objects, queries, and mutations, and seamlessly mix them with SeaORM models ++ Generate ready-to-run GraphQL servers via the included CLI, supporting different web frameworks out of the box ++ Implement role-based access control (RBAC), guards, and lifecycle hooks for fine-grained authorization and business logic ## Supported technologies @@ -36,7 +36,7 @@ Seaography is built on top of SeaORM, so it supports: ### Web framework -It's easy to integrate Seaography with any web framework, but we ship with the following examples out-of-the-box: +It's easy to integrate Seaography with any web framework, and we ship with the following examples out-of-the-box: + [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite) + [Loco (SeaORM)](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_seaography), [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) @@ -47,6 +47,7 @@ It's easy to integrate Seaography with any web framework, but we ship with the f * Relational query (1-to-1, 1-to-N, M-to-N) * Pagination for queries and relations * Filtering with operators (e.g. gt, lt, eq) +* Filter by related entities * Order by any column * Mutations (create, update, delete) * Field guards on entity / column to restrict access @@ -235,7 +236,7 @@ Find all inactive customers, include their address, and their payments with amou } ``` -### Filter using Postgres enum +### Filter using MySQL / Postgres enum ```graphql { film( diff --git a/src/lib.rs b/src/lib.rs index e20ffe9f..fdd3706d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,8 @@ //! //! Seaography logo //! -//!

-//! 🧭 A GraphQL framework for SeaORM -//!

+//!

🧭 A GraphQL framework for Rust

+//!

Quick to start, type‑safe, powerful and extensible

//! //! [![crate](https://img.shields.io/crates/v/seaography.svg)](https://crates.io/crates/seaography) //! [![docs](https://docs.rs/seaography/badge.svg)](https://docs.rs/seaography) @@ -14,16 +13,17 @@ //! //! # Seaography //! -//! Seaography is a GraphQL framework that bridges async-graphql and SeaORM, instantly turning your database into a fully functional GraphQL API in Rust. -//! It leverages async‑graphql's dynamic schema capabilities, resulting in minimal generated code and faster compile times compared to static schemas. -//! With extensive configuration options, you can easily tailor the generated GraphQL schema to your application's needs. +//! Seaography is a **GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), +//! turning your database schema into a fully-typed GraphQL API with minimal effort. +//! By leveraging async-graphql's dynamic schema engine, Seaography avoids the heavy code generation of static approaches, resulting in faster compile times. +//! The generated schema stays in sync with your SeaORM entities, while still giving you full control to extend and customize it. //! -//! Seaography enables you to focus on your application logic instead of boilerplate. -//! With Seaography, you can: +//! With Seaography you can focus on application logic instead of boilerplate. It enables you to: //! -//! + Turn a set of SeaORM entities into a complete GraphQL schema -//! + Use derive macros to craft custom input / output objects, queries and mutations, mix-and-match them with SeaORM models -//! + Generate web servers with the included CLI - ready to compile and run +//! + Expose a complete GraphQL schema directly from your SeaORM entities, including filters, pagination, and nested relations +//! + Use derive macros to define custom input/output objects, queries, and mutations, and seamlessly mix them with SeaORM models +//! + Generate ready-to-run GraphQL servers via the included CLI, supporting different web frameworks out of the box +//! + Implement role-based access control (RBAC), guards, and lifecycle hooks for fine-grained authorization and business logic //! //! ## Supported technologies //! @@ -36,7 +36,7 @@ //! //! ### Web framework //! -//! It's easy to integrate Seaography with any web framework, but we ship with the following examples out-of-the-box: +//! It's easy to integrate Seaography with any web framework, and we ship with the following examples out-of-the-box: //! //! + [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite) //! + [Loco (SeaORM)](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_seaography), [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) @@ -47,6 +47,7 @@ //! * Relational query (1-to-1, 1-to-N, M-to-N) //! * Pagination for queries and relations //! * Filtering with operators (e.g. gt, lt, eq) +//! * Filter by related entities //! * Order by any column //! * Mutations (create, update, delete) //! * Field guards on entity / column to restrict access @@ -235,7 +236,7 @@ //! } //! ``` //! -//! ### Filter using Postgres enum +//! ### Filter using MySQL / Postgres enum //! ```graphql //! { //! film( From f78b97022042191a430c1db1efcc068d7a131b71 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 23 Oct 2025 18:55:26 +0100 Subject: [PATCH 43/85] Update Actix template --- examples/mysql/src/main.rs | 10 +++++----- generator/src/templates/actix.rs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/mysql/src/main.rs b/examples/mysql/src/main.rs index 4ffc767b..8e66ab60 100644 --- a/examples/mysql/src/main.rs +++ b/examples/mysql/src/main.rs @@ -23,16 +23,16 @@ lazy_static! { }); } -async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() -} - async fn graphql_playground() -> Result { Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT)))) } +async fn graphql_handler(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() +} + #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); @@ -50,12 +50,12 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() .app_data(Data::new(schema.clone())) - .service(web::resource("/").guard(guard::Post()).to(index)) .service( web::resource("/") .guard(guard::Get()) .to(graphql_playground), ) + .service(web::resource("/").guard(guard::Post()).to(graphql_handler)) }) .bind("127.0.0.1:8000")? .run() diff --git a/generator/src/templates/actix.rs b/generator/src/templates/actix.rs index a5225475..74db2e1b 100644 --- a/generator/src/templates/actix.rs +++ b/generator/src/templates/actix.rs @@ -35,16 +35,16 @@ pub fn generate_main(crate_name: &str) -> TokenStream { }); } - async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() - } - async fn graphql_playground() -> Result { Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT)))) } + async fn graphql_handler(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() + } + #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); @@ -60,8 +60,8 @@ pub fn generate_main(crate_name: &str) -> TokenStream { HttpServer::new(move || { App::new() .app_data(Data::new(schema.clone())) - .service(web::resource("/").guard(guard::Post()).to(index)) .service(web::resource("/").guard(guard::Get()).to(graphql_playground)) + .service(web::resource("/").guard(guard::Post()).to(graphql_handler)) }) .bind("127.0.0.1:8000")? .run() From 0ba89893216350dddf413010383bbefefc7df911 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 23 Oct 2025 20:49:52 +0100 Subject: [PATCH 44/85] Small tweak --- examples/sqlite/tests/hook_guard_tests.rs | 1 - src/custom/types.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/examples/sqlite/tests/hook_guard_tests.rs b/examples/sqlite/tests/hook_guard_tests.rs index 1b806ca7..815d912d 100644 --- a/examples/sqlite/tests/hook_guard_tests.rs +++ b/examples/sqlite/tests/hook_guard_tests.rs @@ -75,7 +75,6 @@ struct Permissions { staff: BTreeSet, } -#[derive(Clone)] struct MyHooks; impl Log { diff --git a/src/custom/types.rs b/src/custom/types.rs index 9cd77fa2..dc2adea5 100644 --- a/src/custom/types.rs +++ b/src/custom/types.rs @@ -67,10 +67,6 @@ pub trait CustomUnion { fn to_union() -> Union; } -pub trait CustomOperation { - fn to_fields() -> Vec; -} - pub trait GqlScalarValueType: Sized { fn gql_type_ref(ctx: &'static BuilderContext) -> TypeRef; From 8269b70f92ec407253350ace391986037b00b702 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Fri, 24 Oct 2025 12:50:22 +0100 Subject: [PATCH 45/85] README --- README.md | 28 +++++++++++++++++++--------- src/lib.rs | 28 +++++++++++++++++++--------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5e0b95e3..46bd62e5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ # Seaography +## Introduction + Seaography is a **GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), turning your database schema into a fully-typed GraphQL API with minimal effort. By leveraging async-graphql's dynamic schema engine, Seaography avoids the heavy code generation of static approaches, resulting in faster compile times. @@ -23,7 +25,7 @@ With Seaography you can focus on application logic instead of boilerplate. It en + Expose a complete GraphQL schema directly from your SeaORM entities, including filters, pagination, and nested relations + Use derive macros to define custom input/output objects, queries, and mutations, and seamlessly mix them with SeaORM models + Generate ready-to-run GraphQL servers via the included CLI, supporting different web frameworks out of the box -+ Implement role-based access control (RBAC), guards, and lifecycle hooks for fine-grained authorization and business logic ++ Use RBAC, guards, and lifecycle hooks to implement authorization and custom business logic ## Supported technologies @@ -41,24 +43,32 @@ It's easy to integrate Seaography with any web framework, and we ship with the f + [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite) + [Loco (SeaORM)](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_seaography), [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) +### SeaORM Version Compatibility + +| Seaography | SeaORM | +|----------------------------------------------------------|-------------------------------------------------------| +| [2.0](https://crates.io/crates/seaography/2.0.0-rc) | [2.0](https://crates.io/crates/sea-orm/2.0.0-rc) | +| [1.1](https://crates.io/crates/seaography/1.1.4) | [1.1](https://crates.io/crates/sea-orm/1.1.13) | + ## Features * Rich types support (e.g. DateTime, Decimal) * Relational query (1-to-1, 1-to-N, M-to-N) -* Pagination for queries and relations +* Offset-based and cursor-based pagination * Filtering with operators (e.g. gt, lt, eq) * Filter by related entities * Order by any column * Mutations (create, update, delete) -* Field guards on entity / column to restrict access -* Choose between camel or snake case, and singular or plural field names +* Guards and Filters on entity to restrict access +* Choose between camel or snake case field names -## SeaORM Version Compatibility +### Extensible -| Seaography | SeaORM | -|----------------------------------------------------------|-------------------------------------------------------| -| [2.0](https://crates.io/crates/seaography/2.0.0-rc) | [2.0](https://crates.io/crates/sea-orm/2.0.0-rc) | -| [1.1](https://crates.io/crates/seaography/1.1.4) | [1.1](https://crates.io/crates/sea-orm/1.1.13) | +Seaography is also completely extensible. It offers: + +* Extensive configuration options in schema builder +* Lifecycle hooks for custom resolver logic +* Add custom queries & mutations with derive macros ## Quick start - ready to serve in 3 minutes! diff --git a/src/lib.rs b/src/lib.rs index fdd3706d..289f10a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ //! //! # Seaography //! +//! ## Introduction +//! //! Seaography is a **GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), //! turning your database schema into a fully-typed GraphQL API with minimal effort. //! By leveraging async-graphql's dynamic schema engine, Seaography avoids the heavy code generation of static approaches, resulting in faster compile times. @@ -23,7 +25,7 @@ //! + Expose a complete GraphQL schema directly from your SeaORM entities, including filters, pagination, and nested relations //! + Use derive macros to define custom input/output objects, queries, and mutations, and seamlessly mix them with SeaORM models //! + Generate ready-to-run GraphQL servers via the included CLI, supporting different web frameworks out of the box -//! + Implement role-based access control (RBAC), guards, and lifecycle hooks for fine-grained authorization and business logic +//! + Use RBAC, guards, and lifecycle hooks to implement authorization and custom business logic //! //! ## Supported technologies //! @@ -41,24 +43,32 @@ //! + [Actix](https://github.com/SeaQL/seaography/tree/1.1.x/examples/mysql), [Axum](https://github.com/SeaQL/seaography/tree/1.1.x/examples/postgres), [Poem](https://github.com/SeaQL/seaography/tree/1.1.x/examples/sqlite) //! + [Loco (SeaORM)](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_seaography), [Loco (SeaORM Pro)](https://github.com/SeaQL/sea-orm-pro) //! +//! ### SeaORM Version Compatibility +//! +//! | Seaography | SeaORM | +//! |----------------------------------------------------------|-------------------------------------------------------| +//! | [2.0](https://crates.io/crates/seaography/2.0.0-rc) | [2.0](https://crates.io/crates/sea-orm/2.0.0-rc) | +//! | [1.1](https://crates.io/crates/seaography/1.1.4) | [1.1](https://crates.io/crates/sea-orm/1.1.13) | +//! //! ## Features //! //! * Rich types support (e.g. DateTime, Decimal) //! * Relational query (1-to-1, 1-to-N, M-to-N) -//! * Pagination for queries and relations +//! * Offset-based and cursor-based pagination //! * Filtering with operators (e.g. gt, lt, eq) //! * Filter by related entities //! * Order by any column //! * Mutations (create, update, delete) -//! * Field guards on entity / column to restrict access -//! * Choose between camel or snake case, and singular or plural field names +//! * Guards and Filters on entity to restrict access +//! * Choose between camel or snake case field names //! -//! ## SeaORM Version Compatibility +//! ### Extensible //! -//! | Seaography | SeaORM | -//! |----------------------------------------------------------|-------------------------------------------------------| -//! | [2.0](https://crates.io/crates/seaography/2.0.0-rc) | [2.0](https://crates.io/crates/sea-orm/2.0.0-rc) | -//! | [1.1](https://crates.io/crates/seaography/1.1.4) | [1.1](https://crates.io/crates/sea-orm/1.1.13) | +//! Seaography is also completely extensible. It offers: +//! +//! * Extensive configuration options in schema builder +//! * Lifecycle hooks for custom resolver logic +//! * Add custom queries & mutations with derive macros //! //! ## Quick start - ready to serve in 3 minutes! //! From 1b3ebccc47eb15660f21f277be3af732389350e0 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 20 Oct 2025 09:10:55 +0100 Subject: [PATCH 46/85] Refactor loader --- src/query/entity_object_relation.rs | 35 ++- src/query/entity_object_via_relation.rs | 40 ++-- src/query/loader.rs | 294 +++++++----------------- src/query/loader/impl_traits.rs | 193 ++++++++++++++++ 4 files changed, 311 insertions(+), 251 deletions(-) create mode 100644 src/query/loader/impl_traits.rs diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index be401e2c..2f8b7a6f 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -4,7 +4,8 @@ use async_graphql::{ }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ - DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, QueryTrait, RelationDef, + sea_query::ValueTuple, DatabaseConnection, EntityTrait, Identity, ModelTrait, QueryFilter, + QueryTrait, RelationDef, }; use crate::{ @@ -63,23 +64,17 @@ impl EntityObjectRelationBuilder { let object_name_ = object_name.clone(); let hooks = &self.context.hooks; - let from_col = ::from_str( - relation_definition - .from_col - .to_string() - .to_snake_case() - .as_str(), - ) - .unwrap_or_else(|_| panic!("Illegal from_col: {:?}", relation_definition.from_col)); - - let to_col = ::from_str( - relation_definition - .to_col - .to_string() - .to_snake_case() - .as_str(), - ) - .unwrap_or_else(|_| panic!("Illegal to_col: {:?}", relation_definition.to_col)); + let from_col = match relation_definition.from_col.clone() { + Identity::Unary(iden) => ::from_str(&iden.inner()) + .unwrap_or_else(|_| panic!("Illegal from_col: {:?}", relation_definition.from_col)), + _ => todo!("Unsupported composite key"), + }; + + let to_col = match relation_definition.to_col.clone() { + Identity::Unary(iden) => ::from_str(&iden.inner()) + .unwrap_or_else(|_| panic!("Illegal to_col: {:?}", relation_definition.to_col)), + _ => todo!("Unsupported composite key"), + }; let field_name = name.clone(); let field = match relation_definition.is_owner { @@ -126,7 +121,7 @@ impl EntityObjectRelationBuilder { let order_by = ctx.args.get(&context.entity_query_field.order_by); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; let key = KeyComplex:: { - key: vec![parent.get(from_col)], + key: ValueTuple::One(parent.get(from_col)), meta: HashableGroupKey:: { stmt, columns: vec![to_col], @@ -191,7 +186,7 @@ impl EntityObjectRelationBuilder { let order_by = ctx.args.get(&context.entity_query_field.order_by); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; let key = KeyComplex:: { - key: vec![parent.get(from_col)], + key: ValueTuple::One(parent.get(from_col)), meta: HashableGroupKey:: { stmt, columns: vec![to_col], diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 68527477..6eed562e 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -4,8 +4,8 @@ use async_graphql::{ }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ - ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, - QueryTrait, Related, RelationDef, + sea_query::ValueTuple, ColumnTrait, Condition, DatabaseConnection, EntityTrait, Identity, + ModelTrait, QueryFilter, QueryTrait, Related, RelationDef, }; use crate::{ @@ -72,23 +72,21 @@ impl EntityObjectViaRelationBuilder { let object_name_ = object_name.clone(); let hooks = &self.context.hooks; - let from_col = ::from_str( - via_relation_definition - .from_col - .to_string() - .to_snake_case() - .as_str(), - ) - .unwrap_or_else(|_| panic!("Illegal from_col: {:?}", via_relation_definition.from_col)); - - let to_col = ::from_str( - to_relation_definition - .to_col - .to_string() - .to_snake_case() - .as_str(), - ) - .unwrap_or_else(|_| panic!("Illegal from_col: {:?}", to_relation_definition.to_col)); + let from_col = match via_relation_definition.from_col.clone() { + Identity::Unary(iden) => ::from_str(&iden.inner()) + .unwrap_or_else(|_| { + panic!("Illegal from_col: {:?}", via_relation_definition.from_col) + }), + _ => todo!("Unsupported composite key"), + }; + + let to_col = match to_relation_definition.to_col.clone() { + Identity::Unary(iden) => ::from_str(&iden.inner()) + .unwrap_or_else(|_| { + panic!("Illegal from_col: {:?}", to_relation_definition.to_col) + }), + _ => todo!("Unsupported composite key"), + }; let field_name = name.clone(); let field = match via_relation_definition.is_owner { @@ -139,7 +137,7 @@ impl EntityObjectViaRelationBuilder { let order_by = ctx.args.get(&context.entity_query_field.order_by); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; let key = KeyComplex:: { - key: vec![parent.get(from_col)], + key: ValueTuple::One(parent.get(from_col)), meta: HashableGroupKey:: { stmt, columns: vec![to_col], @@ -224,7 +222,7 @@ impl EntityObjectViaRelationBuilder { db.user_can_run(stmt.as_query())?; let key = KeyComplex:: { - key: vec![parent.get(from_col)], + key: ValueTuple::One(parent.get(from_col)), meta: HashableGroupKey:: { stmt, columns: vec![to_col], diff --git a/src/query/loader.rs b/src/query/loader.rs index 8d00f56e..5275aa41 100644 --- a/src/query/loader.rs +++ b/src/query/loader.rs @@ -1,4 +1,10 @@ -use sea_orm::{sea_query::ValueTuple, Condition, ExprTrait, ModelTrait, QueryFilter}; +mod impl_traits; +use impl_traits::*; + +use sea_orm::{ + sea_query::{Value, ValueTuple}, + Condition, EntityTrait, ExprTrait, ModelTrait, QueryFilter, +}; use std::{collections::HashMap, hash::Hash, marker::PhantomData, sync::Arc}; use crate::apply_order; @@ -6,101 +12,18 @@ use crate::apply_order; #[derive(Clone, Debug)] pub struct KeyComplex where - T: sea_orm::EntityTrait, + T: EntityTrait, { /// The key tuple to equal with columns - pub key: Vec, + pub key: ValueTuple, /// Meta Information pub meta: HashableGroupKey, } -impl PartialEq for KeyComplex -where - T: sea_orm::EntityTrait, -{ - fn eq(&self, other: &Self) -> bool { - self.key - .iter() - .map(map_key) - .eq(other.key.iter().map(map_key)) - && self.meta.eq(&other.meta) - } -} - -fn map_key(key: &sea_orm::Value) -> sea_orm::Value { - match key { - sea_orm::Value::TinyInt(value) => { - let value: Option = value.map(|value| value as i64); - sea_orm::Value::BigInt(value) - } - sea_orm::Value::SmallInt(value) => { - let value: Option = value.map(|value| value as i64); - sea_orm::Value::BigInt(value) - } - sea_orm::Value::Int(value) => { - let value: Option = value.map(|value| value as i64); - sea_orm::Value::BigInt(value) - } - sea_orm::Value::TinyUnsigned(value) => { - let value: Option = value.map(|value| value as u64); - sea_orm::Value::BigUnsigned(value) - } - sea_orm::Value::SmallUnsigned(value) => { - let value: Option = value.map(|value| value as u64); - sea_orm::Value::BigUnsigned(value) - } - sea_orm::Value::Unsigned(value) => { - let value: Option = value.map(|value| value as u64); - sea_orm::Value::BigUnsigned(value) - } - _ => key.clone(), - } -} - -impl Eq for KeyComplex where T: sea_orm::EntityTrait {} - -impl Hash for KeyComplex -where - T: sea_orm::EntityTrait, -{ - fn hash(&self, state: &mut H) { - for key in self.key.iter() { - match key { - sea_orm::Value::TinyInt(value) => { - let value: Option = value.map(|value| value as i64); - value.hash(state); - } - sea_orm::Value::SmallInt(value) => { - let value: Option = value.map(|value| value as i64); - value.hash(state); - } - sea_orm::Value::Int(value) => { - let value: Option = value.map(|value| value as i64); - value.hash(state); - } - sea_orm::Value::TinyUnsigned(value) => { - let value: Option = value.map(|value| value as u64); - value.hash(state); - } - sea_orm::Value::SmallUnsigned(value) => { - let value: Option = value.map(|value| value as u64); - value.hash(state); - } - sea_orm::Value::Unsigned(value) => { - let value: Option = value.map(|value| value as u64); - value.hash(state); - } - _ => key.hash(state), - } - } - self.meta.hash(state); - } -} - #[derive(Clone, Debug)] pub struct HashableGroupKey where - T: sea_orm::EntityTrait, + T: EntityTrait, { /// Foundation SQL statement pub stmt: sea_orm::Select, @@ -112,64 +35,14 @@ where pub order_by: Vec<(T::Column, sea_orm::sea_query::Order)>, } -impl PartialEq for HashableGroupKey -where - T: sea_orm::EntityTrait, -{ - fn eq(&self, other: &Self) -> bool { - self.filters.eq(&other.filters) - && std::cmp::PartialEq::eq( - &format!("{:?}", self.columns), - &format!("{:?}", other.columns), - ) - && std::cmp::PartialEq::eq( - &format!("{:?}", self.order_by), - &format!("{:?}", other.order_by), - ) - } -} - -impl Eq for HashableGroupKey where T: sea_orm::EntityTrait {} - -impl Hash for HashableGroupKey -where - T: sea_orm::EntityTrait, -{ - fn hash(&self, state: &mut H) { - format!("{:?}", self.filters).hash(state); - format!("{:?}", self.columns).hash(state); - format!("{:?}", self.order_by).hash(state); - } -} - #[derive(Clone, Debug)] pub struct HashableColumn(T::Column) where - T: sea_orm::EntityTrait; - -impl PartialEq for HashableColumn -where - T: sea_orm::EntityTrait, -{ - fn eq(&self, other: &Self) -> bool { - std::cmp::PartialEq::eq(&format!("{:?}", self.0), &format!("{:?}", other.0)) - } -} - -impl Eq for HashableColumn where T: sea_orm::EntityTrait {} - -impl Hash for HashableColumn -where - T: sea_orm::EntityTrait, -{ - fn hash(&self, state: &mut H) { - format!("{:?}", self.0).hash(state); - } -} + T: EntityTrait; pub struct OneToManyLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, { connection: sea_orm::DatabaseConnection, entity: PhantomData, @@ -177,7 +50,7 @@ where impl OneToManyLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { pub fn new(connection: sea_orm::DatabaseConnection) -> Self { @@ -190,7 +63,7 @@ where impl async_graphql::dataloader::Loader> for OneToManyLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { type Value = Vec; @@ -200,7 +73,7 @@ where &self, keys: &[KeyComplex], ) -> Result, Self::Value>, Self::Error> { - let items: HashMap, Vec>> = keys + let items: HashMap, Vec> = keys .iter() .cloned() .map(|item: KeyComplex| { @@ -215,9 +88,9 @@ where ) }) .fold( - HashMap::, Vec>>::new(), - |mut acc: HashMap, Vec>>, - cur: (HashableGroupKey, Vec)| { + HashMap::, Vec>::new(), + |mut acc: HashMap, Vec>, + cur: (HashableGroupKey, ValueTuple)| { match acc.get_mut(&cur.0) { Some(items) => { items.push(cur.1); @@ -233,30 +106,22 @@ where let promises: HashMap, _> = items .into_iter() - .map( - |(key, values): (HashableGroupKey, Vec>)| { - let cloned_key = key.clone(); - - let stmt = key.stmt; - - let condition = match key.filters { - Some(condition) => Condition::all().add(condition), - None => Condition::all(), - }; - let tuple = sea_orm::sea_query::Expr::tuple( - key.columns - .iter() - .map(|column: &T::Column| sea_orm::sea_query::Expr::col(*column)), - ); - let condition = - condition.add(tuple.in_tuples(values.into_iter().map(ValueTuple::Many))); - let stmt = stmt.filter(condition); - - let stmt = apply_order(stmt, key.order_by); - - (cloned_key, stmt.all(&self.connection)) - }, - ) + .map(|(key, values): (HashableGroupKey, Vec)| { + let cloned_key = key.clone(); + + let stmt = key.stmt; + + let condition = match key.filters { + Some(condition) => Condition::all().add(condition), + None => Condition::all(), + }; + let condition = apply_condition(condition, &key.columns, values); + let stmt = stmt.filter(condition); + + let stmt = apply_order(stmt, key.order_by); + + (cloned_key, stmt.all(&self.connection)) + }) .collect(); let mut results: HashMap, Vec> = HashMap::new(); @@ -266,11 +131,7 @@ where let result: Vec = promise.await.map_err(Arc::new)?; for item in result.into_iter() { let key = &KeyComplex:: { - key: key - .columns - .iter() - .map(|col: &T::Column| item.get(*col)) - .collect(), + key: collect_key(key.columns.iter().map(|col: &T::Column| item.get(*col))), meta: key.clone(), }; match results.get_mut(key) { @@ -290,7 +151,7 @@ where pub struct OneToOneLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, { connection: sea_orm::DatabaseConnection, entity: PhantomData, @@ -298,7 +159,7 @@ where impl OneToOneLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { pub fn new(connection: sea_orm::DatabaseConnection) -> Self { @@ -311,7 +172,7 @@ where impl async_graphql::dataloader::Loader> for OneToOneLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { type Value = T::Model; @@ -321,7 +182,7 @@ where &self, keys: &[KeyComplex], ) -> Result, Self::Value>, Self::Error> { - let items: HashMap, Vec>> = keys + let items: HashMap, Vec> = keys .iter() .cloned() .map(|item: KeyComplex| { @@ -336,9 +197,9 @@ where ) }) .fold( - HashMap::, Vec>>::new(), - |mut acc: HashMap, Vec>>, - cur: (HashableGroupKey, Vec)| { + HashMap::, Vec>::new(), + |mut acc: HashMap, Vec>, + cur: (HashableGroupKey, ValueTuple)| { match acc.get_mut(&cur.0) { Some(items) => { items.push(cur.1); @@ -354,30 +215,22 @@ where let promises: HashMap, _> = items .into_iter() - .map( - |(key, values): (HashableGroupKey, Vec>)| { - let cloned_key = key.clone(); - - let stmt = key.stmt; - - let condition = match key.filters { - Some(condition) => Condition::all().add(condition), - None => Condition::all(), - }; - let tuple = sea_orm::sea_query::Expr::tuple( - key.columns - .iter() - .map(|column: &T::Column| sea_orm::sea_query::Expr::col(*column)), - ); - let condition = - condition.add(tuple.in_tuples(values.into_iter().map(ValueTuple::Many))); - let stmt = stmt.filter(condition); - - let stmt = apply_order(stmt, key.order_by); - - (cloned_key, stmt.all(&self.connection)) - }, - ) + .map(|(key, values): (HashableGroupKey, Vec)| { + let cloned_key = key.clone(); + + let stmt = key.stmt; + + let condition = match key.filters { + Some(condition) => Condition::all().add(condition), + None => Condition::all(), + }; + let condition = apply_condition(condition, &key.columns, values); + let stmt = stmt.filter(condition); + + let stmt = apply_order(stmt, key.order_by); + + (cloned_key, stmt.all(&self.connection)) + }) .collect(); let mut results: HashMap, T::Model> = HashMap::new(); @@ -387,11 +240,7 @@ where let result: Vec = promise.await.map_err(Arc::new)?; for item in result.into_iter() { let key = &KeyComplex:: { - key: key - .columns - .iter() - .map(|col: &T::Column| item.get(*col)) - .collect(), + key: collect_key(key.columns.iter().map(|col: &T::Column| item.get(*col))), meta: key.clone(), }; results.insert(key.clone(), item); @@ -401,3 +250,28 @@ where Ok(results) } } + +fn apply_condition( + condition: Condition, + cols: &[T], + values: Vec, +) -> Condition { + if cols.len() == 1 { + condition.add( + sea_orm::sea_query::Expr::col(cols[0]).is_in(values.into_iter().map( + |tuple| match tuple { + ValueTuple::One(v) => v, + ValueTuple::Two(v, _) => v, + ValueTuple::Three(v, _, _) => v, + _ => panic!("Column & Value arity mismatch: expected 1"), + }, + )), + ) + } else { + let tuple = sea_orm::sea_query::Expr::tuple( + cols.iter() + .map(|column| sea_orm::sea_query::Expr::col(*column)), + ); + condition.add(tuple.in_tuples(values)) + } +} diff --git a/src/query/loader/impl_traits.rs b/src/query/loader/impl_traits.rs new file mode 100644 index 00000000..77da05b5 --- /dev/null +++ b/src/query/loader/impl_traits.rs @@ -0,0 +1,193 @@ +use super::*; + +impl PartialEq for KeyComplex +where + T: sea_orm::EntityTrait, +{ + fn eq(&self, other: &Self) -> bool { + ValueTupleIter::new(&self.key) + .map(map_key) + .eq(ValueTupleIter::new(&other.key).map(map_key)) + && self.meta.eq(&other.meta) + } +} + +fn map_key(key: &sea_orm::Value) -> sea_orm::Value { + match key { + sea_orm::Value::TinyInt(value) => { + let value: Option = value.map(|value| value as i64); + sea_orm::Value::BigInt(value) + } + sea_orm::Value::SmallInt(value) => { + let value: Option = value.map(|value| value as i64); + sea_orm::Value::BigInt(value) + } + sea_orm::Value::Int(value) => { + let value: Option = value.map(|value| value as i64); + sea_orm::Value::BigInt(value) + } + sea_orm::Value::TinyUnsigned(value) => { + let value: Option = value.map(|value| value as u64); + sea_orm::Value::BigUnsigned(value) + } + sea_orm::Value::SmallUnsigned(value) => { + let value: Option = value.map(|value| value as u64); + sea_orm::Value::BigUnsigned(value) + } + sea_orm::Value::Unsigned(value) => { + let value: Option = value.map(|value| value as u64); + sea_orm::Value::BigUnsigned(value) + } + _ => key.clone(), + } +} + +impl Eq for KeyComplex where T: sea_orm::EntityTrait {} + +impl Hash for KeyComplex +where + T: sea_orm::EntityTrait, +{ + fn hash(&self, state: &mut H) { + for key in ValueTupleIter::new(&self.key) { + match key { + sea_orm::Value::TinyInt(value) => { + let value: Option = value.map(|value| value as i64); + value.hash(state); + } + sea_orm::Value::SmallInt(value) => { + let value: Option = value.map(|value| value as i64); + value.hash(state); + } + sea_orm::Value::Int(value) => { + let value: Option = value.map(|value| value as i64); + value.hash(state); + } + sea_orm::Value::TinyUnsigned(value) => { + let value: Option = value.map(|value| value as u64); + value.hash(state); + } + sea_orm::Value::SmallUnsigned(value) => { + let value: Option = value.map(|value| value as u64); + value.hash(state); + } + sea_orm::Value::Unsigned(value) => { + let value: Option = value.map(|value| value as u64); + value.hash(state); + } + _ => key.hash(state), + } + } + self.meta.hash(state); + } +} + +impl PartialEq for HashableGroupKey +where + T: sea_orm::EntityTrait, +{ + fn eq(&self, other: &Self) -> bool { + self.filters.eq(&other.filters) + && std::cmp::PartialEq::eq( + &format!("{:?}", self.columns), + &format!("{:?}", other.columns), + ) + && std::cmp::PartialEq::eq( + &format!("{:?}", self.order_by), + &format!("{:?}", other.order_by), + ) + } +} + +impl Eq for HashableGroupKey where T: sea_orm::EntityTrait {} + +impl Hash for HashableGroupKey +where + T: sea_orm::EntityTrait, +{ + fn hash(&self, state: &mut H) { + format!("{:?}", self.filters).hash(state); + format!("{:?}", self.columns).hash(state); + format!("{:?}", self.order_by).hash(state); + } +} + +impl PartialEq for HashableColumn +where + T: sea_orm::EntityTrait, +{ + fn eq(&self, other: &Self) -> bool { + std::cmp::PartialEq::eq(&format!("{:?}", self.0), &format!("{:?}", other.0)) + } +} + +impl Eq for HashableColumn where T: sea_orm::EntityTrait {} + +impl Hash for HashableColumn +where + T: sea_orm::EntityTrait, +{ + fn hash(&self, state: &mut H) { + format!("{:?}", self.0).hash(state); + } +} + +pub struct ValueTupleIter<'a> { + key: &'a ValueTuple, + index: usize, +} + +impl<'a> ValueTupleIter<'a> { + pub fn new(key: &'a ValueTuple) -> Self { + Self { key, index: 0 } + } +} + +impl<'a> Iterator for ValueTupleIter<'a> { + type Item = &'a Value; + + fn next(&mut self) -> Option { + let result = match self.key { + ValueTuple::One(iden1) => { + if self.index == 0 { + Some(iden1) + } else { + None + } + } + ValueTuple::Two(iden1, iden2) => match self.index { + 0 => Some(iden1), + 1 => Some(iden2), + _ => None, + }, + ValueTuple::Three(iden1, iden2, iden3) => match self.index { + 0 => Some(iden1), + 1 => Some(iden2), + 2 => Some(iden3), + _ => None, + }, + ValueTuple::Many(vec) => vec.get(self.index), + _ => unreachable!(), + }; + self.index += 1; + result + } +} + +pub fn collect_key(mut it: impl Iterator) -> ValueTuple { + match (it.next(), it.next(), it.next(), it.next()) { + (Some(a), None, _, _) => ValueTuple::One(a), + (Some(a), Some(b), None, _) => ValueTuple::Two(a, b), + (Some(a), Some(b), Some(c), None) => ValueTuple::Three(a, b, c), + (Some(a), Some(b), Some(c), Some(d)) => { + // collect remaining into vec + let mut v = vec![a, b, c, d]; + v.extend(it); + ValueTuple::Many(v) + } + (None, _, _, _) => { + // empty iterator + ValueTuple::Many(Vec::new()) + } + } +} From 6869ee8674eef23e74d040966fb54f0505a93a6c Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 20 Oct 2025 17:56:39 +0100 Subject: [PATCH 47/85] More test cases --- examples/sqlite/tests/query_tests.rs | 231 +++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index 6e414df1..e7a46ac7 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -1380,3 +1380,234 @@ async fn film_filter_or() { "#, ) } + +#[tokio::test] +async fn city_with_address() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + city(filters: { cityId: { is_in: [1, 2, 3] } }) { + nodes { + cityId + city + address { + nodes { + addressId + address + } + } + } + } + } + "#, + ) + .await, + r#" + { + "city": { + "nodes": [ + { + "cityId": 1, + "city": "A Corua (La Corua)", + "address": { + "nodes": [ + { + "addressId": 56, + "address": "939 Probolinggo Loop" + } + ] + } + }, + { + "cityId": 2, + "city": "Abha", + "address": { + "nodes": [ + { + "addressId": 105, + "address": "733 Mandaluyong Place" + } + ] + } + }, + { + "cityId": 3, + "city": "Abu Dhabi", + "address": { + "nodes": [ + { + "addressId": 457, + "address": "535 Ahmadnagar Manor" + } + ] + } + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + address(filters: { addressId: { is_in: [1, 2, 3] } }) { + nodes { + addressId + address + city { + cityId + city + } + } + } + } + "#, + ) + .await, + r#" + { + "address": { + "nodes": [ + { + "addressId": 1, + "address": "47 MySakila Drive", + "city": { + "cityId": 300, + "city": "Lethbridge" + } + }, + { + "addressId": 2, + "address": "28 MySQL Boulevard", + "city": { + "cityId": 576, + "city": "Woodridge" + } + }, + { + "addressId": 3, + "address": "23 Workhaven Lane", + "city": { + "cityId": 300, + "city": "Lethbridge" + } + } + ] + } + } + "#, + ); +} + +#[tokio::test] +async fn actor_to_film() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + actor(filters: { actorId: { eq: 35 } }) { + nodes { + actorId + firstName + lastName + film { + nodes { + filmId + title + } + } + } + } + } + "#, + ) + .await, + r#" + { + "actor": { + "nodes": [ + { + "actorId": 35, + "firstName": "JUDY", + "lastName": "DEAN", + "film": { + "nodes": [ + { + "filmId": 10, + "title": "ALADDIN CALENDAR" + }, + { + "filmId": 35, + "title": "ARACHNOPHOBIA ROLLERCOASTER" + }, + { + "filmId": 52, + "title": "BALLROOM MOCKINGBIRD" + }, + { + "filmId": 201, + "title": "CYCLONE FAMILY" + }, + { + "filmId": 256, + "title": "DROP WATERFRONT" + }, + { + "filmId": 389, + "title": "GUNFIGHTER MUSSOLINI" + }, + { + "filmId": 589, + "title": "MODERN DORADO" + }, + { + "filmId": 612, + "title": "MUSSOLINI SPOILERS" + }, + { + "filmId": 615, + "title": "NASH CHOCOLAT" + }, + { + "filmId": 707, + "title": "QUEST MUSSOLINI" + }, + { + "filmId": 732, + "title": "RINGS HEARTBREAKERS" + }, + { + "filmId": 738, + "title": "ROCKETEER MOTHER" + }, + { + "filmId": 748, + "title": "RUGRATS SHAKESPEARE" + }, + { + "filmId": 817, + "title": "SOLDIERS EVOLUTION" + }, + { + "filmId": 914, + "title": "TROUBLE DATE" + } + ] + } + } + ] + } + } + "#, + ) +} From 7ca8af0a343f6e25fe841bb9ea8e2b404a1e5b24 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 20 Oct 2025 21:39:29 +0100 Subject: [PATCH 48/85] New Loader --- src/query/entity_object_relation.rs | 51 ++-- src/query/entity_object_via_relation.rs | 123 +++++----- src/query/loader.rs | 238 ++++++------------- src/query/loader/impl_traits.rs | 51 +--- src/query/loader/loader_impl.rs | 300 ++++++++++++++++++++++++ 5 files changed, 469 insertions(+), 294 deletions(-) create mode 100644 src/query/loader/loader_impl.rs diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index 2f8b7a6f..66287091 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -4,15 +4,15 @@ use async_graphql::{ }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ - sea_query::ValueTuple, DatabaseConnection, EntityTrait, Identity, ModelTrait, QueryFilter, + DatabaseConnection, EntityTrait, QueryFilter, QueryTrait, RelationDef, }; use crate::{ - apply_memory_pagination, get_filter_conditions, guard_error, pluralize_unique, BuilderContext, - Connection, ConnectionObjectBuilder, DatabaseContext, EntityObjectBuilder, FilterInputBuilder, - GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, OperationType, - OrderInputBuilder, PaginationInputBuilder, UserContext, + apply_memory_pagination, get_filter_conditions, guard_error, loader_impl, pluralize_unique, + BuilderContext, Connection, ConnectionObjectBuilder, DatabaseContext, EntityObjectBuilder, + FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, + OperationType, OrderInputBuilder, PaginationInputBuilder, UserContext, }; /// This builder produces a GraphQL field for an SeaORM entity relationship @@ -63,18 +63,7 @@ impl EntityObjectRelationBuilder { let object_name: String = entity_object_builder.type_name::(); let object_name_ = object_name.clone(); let hooks = &self.context.hooks; - - let from_col = match relation_definition.from_col.clone() { - Identity::Unary(iden) => ::from_str(&iden.inner()) - .unwrap_or_else(|_| panic!("Illegal from_col: {:?}", relation_definition.from_col)), - _ => todo!("Unsupported composite key"), - }; - - let to_col = match relation_definition.to_col.clone() { - Identity::Unary(iden) => ::from_str(&iden.inner()) - .unwrap_or_else(|_| panic!("Illegal to_col: {:?}", relation_definition.to_col)), - _ => todo!("Unsupported composite key"), - }; + let relation_definition_is_owner = relation_definition.is_owner; let field_name = name.clone(); let field = match relation_definition.is_owner { @@ -82,6 +71,7 @@ impl EntityObjectRelationBuilder { let object_name = object_name.clone(); let parent_name = parent_name.clone(); let field_name = field_name.clone(); + let relation_definition = relation_definition.clone(); FieldFuture::new(async move { if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) @@ -120,12 +110,18 @@ impl EntityObjectRelationBuilder { let filters = get_filter_conditions::(context, filters)?; let order_by = ctx.args.get(&context.entity_query_field.order_by); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; + let key = KeyComplex:: { - key: ValueTuple::One(parent.get(from_col)), + key: loader_impl::extract_key::( + &relation_definition.from_col, + parent, + )?, meta: HashableGroupKey:: { stmt, - columns: vec![to_col], - filters: Some(filters), + junction_fields: Vec::new(), + rel_def: relation_definition, + via_def: None, + filters, order_by, }, }; @@ -146,6 +142,7 @@ impl EntityObjectRelationBuilder { let object_name = object_name.clone(); let parent_name = parent_name.clone(); let field_name = field_name.clone(); + let relation_definition = relation_definition.clone(); let context: &'static BuilderContext = context; FieldFuture::new(async move { if let GuardAction::Block(reason) = @@ -185,12 +182,18 @@ impl EntityObjectRelationBuilder { let filters = get_filter_conditions::(context, filters)?; let order_by = ctx.args.get(&context.entity_query_field.order_by); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; + let key = KeyComplex:: { - key: ValueTuple::One(parent.get(from_col)), + key: loader_impl::extract_key::( + &relation_definition.from_col, + parent, + )?, meta: HashableGroupKey:: { stmt, - columns: vec![to_col], - filters: Some(filters), + junction_fields: Vec::new(), + rel_def: relation_definition, + via_def: None, + filters, order_by, }, }; @@ -210,7 +213,7 @@ impl EntityObjectRelationBuilder { ), }; - match relation_definition.is_owner { + match relation_definition_is_owner { false => field, true => field .argument(InputValue::new( diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 6eed562e..80f566e0 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -4,16 +4,16 @@ use async_graphql::{ }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ - sea_query::ValueTuple, ColumnTrait, Condition, DatabaseConnection, EntityTrait, Identity, - ModelTrait, QueryFilter, QueryTrait, Related, RelationDef, + DatabaseConnection, EntityTrait, + QueryFilter, QueryTrait, Related, RelationDef, }; use crate::{ - apply_memory_pagination, apply_order, apply_pagination, get_filter_conditions, guard_error, - pluralize_unique, BuilderContext, ConnectionObjectBuilder, DatabaseContext, - EntityObjectBuilder, FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, - OneToManyLoader, OneToOneLoader, OperationType, OrderInputBuilder, PaginationInputBuilder, - UserContext, + apply_memory_pagination, get_filter_conditions, guard_error, + loader_impl, pluralize_unique, BuilderContext, Connection, ConnectionObjectBuilder, + DatabaseContext, EntityObjectBuilder, FilterInputBuilder, GuardAction, HashableGroupKey, + KeyComplex, OneToManyLoader, OneToOneLoader, OperationType, OrderInputBuilder, + PaginationInputBuilder, UserContext, }; /// This builder produces a GraphQL field for an SeaORM entity related trait @@ -54,13 +54,14 @@ impl EntityObjectViaRelationBuilder { <::Column as std::str::FromStr>::Err: core::fmt::Debug, <::Column as std::str::FromStr>::Err: core::fmt::Debug, { - let to_relation_definition = >::to(); - let name = self.get_relation_name_for(name, &to_relation_definition); + let to_rel_def = >::to(); + let name = self.get_relation_name_for(name, &to_rel_def); let context: &'static BuilderContext = self.context; - let (via_relation_definition, is_via_relation) = match >::via() { - Some(def) => (def, true), - None => (>::to(), false), + let (via_rel_def, is_via_relation) = match >::via() { + Some(rel_def) => (rel_def, true), + None => (to_rel_def.clone(), false), }; + let via_rel_def_is_owner = via_rel_def.is_owner; let entity_object_builder = EntityObjectBuilder { context }; let connection_object_builder = ConnectionObjectBuilder { context }; @@ -72,28 +73,13 @@ impl EntityObjectViaRelationBuilder { let object_name_ = object_name.clone(); let hooks = &self.context.hooks; - let from_col = match via_relation_definition.from_col.clone() { - Identity::Unary(iden) => ::from_str(&iden.inner()) - .unwrap_or_else(|_| { - panic!("Illegal from_col: {:?}", via_relation_definition.from_col) - }), - _ => todo!("Unsupported composite key"), - }; - - let to_col = match to_relation_definition.to_col.clone() { - Identity::Unary(iden) => ::from_str(&iden.inner()) - .unwrap_or_else(|_| { - panic!("Illegal from_col: {:?}", to_relation_definition.to_col) - }), - _ => todo!("Unsupported composite key"), - }; - let field_name = name.clone(); - let field = match via_relation_definition.is_owner { + let field = match via_rel_def.is_owner { false => Field::new(name, TypeRef::named(&object_name), move |ctx| { let object_name = object_name.clone(); let parent_name = parent_name.clone(); let field_name = field_name.clone(); + let to_rel_def = to_rel_def.clone(); FieldFuture::new(async move { if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) @@ -115,11 +101,7 @@ impl EntityObjectViaRelationBuilder { let loader = ctx.data_unchecked::>>(); - let mut stmt = if >::via().is_some() { - >::find_related() - } else { - R::find() - }; + let mut stmt = R::find(); if let Some(filter) = hooks.entity_filter(&ctx, &object_name, OperationType::Read) { @@ -136,12 +118,15 @@ impl EntityObjectViaRelationBuilder { let filters = get_filter_conditions::(context, filters)?; let order_by = ctx.args.get(&context.entity_query_field.order_by); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; + let key = KeyComplex:: { - key: ValueTuple::One(parent.get(from_col)), + key: loader_impl::extract_key::(&to_rel_def.from_col, parent)?, meta: HashableGroupKey:: { stmt, - columns: vec![to_col], - filters: Some(filters), + junction_fields: Vec::new(), + rel_def: to_rel_def, + via_def: None, + filters, order_by, }, }; @@ -162,6 +147,8 @@ impl EntityObjectViaRelationBuilder { let object_name = object_name.clone(); let parent_name = parent_name.clone(); let field_name = field_name.clone(); + let to_rel_def = to_rel_def.clone(); + let via_rel_def = via_rel_def.clone(); FieldFuture::new(async move { if let GuardAction::Block(reason) = hooks.entity_guard(&ctx, &object_name, OperationType::Read) @@ -184,11 +171,7 @@ impl EntityObjectViaRelationBuilder { ))); }; - let mut stmt = if >::via().is_some() { - >::find_related() - } else { - R::find() - }; + let mut stmt = R::find(); if let Some(filter) = hooks.entity_filter(&ctx, &object_name, OperationType::Read) { @@ -209,32 +192,48 @@ impl EntityObjectViaRelationBuilder { .data::()? .restricted(ctx.data_opt::())?; - let connection = if is_via_relation { - // TODO optimize query - let condition = Condition::all().add(from_col.eq(parent.get(from_col))); + db.user_can_run(stmt.as_query())?; - let stmt = stmt.filter(condition.add(filters)); - let stmt = apply_order(stmt, order_by); - apply_pagination::(context, db, stmt, pagination).await? - } else { - let loader = ctx.data_unchecked::>>(); - - db.user_can_run(stmt.as_query())?; + let loader = ctx.data_unchecked::>>(); - let key = KeyComplex:: { - key: ValueTuple::One(parent.get(from_col)), + let key = if is_via_relation { + KeyComplex:: { + key: loader_impl::extract_key::( + &via_rel_def.from_col, + parent, + )?, meta: HashableGroupKey:: { stmt, - columns: vec![to_col], - filters: Some(filters), + junction_fields: loader_impl::extract_col_type::( + &via_rel_def.from_col, + &via_rel_def.to_col, + )?, + rel_def: to_rel_def, + via_def: Some(via_rel_def), + filters, order_by, }, - }; - - let values = loader.load_one(key).await?; - - apply_memory_pagination(context, values, pagination)? + } + } else { + KeyComplex:: { + key: loader_impl::extract_key::( + &to_rel_def.from_col, + parent, + )?, + meta: HashableGroupKey:: { + stmt, + junction_fields: Vec::new(), + rel_def: to_rel_def, + via_def: None, + filters, + order_by, + }, + } }; + let values = loader.load_one(key).await?; + + let connection: Connection = + apply_memory_pagination(context, values, pagination)?; Ok(Some(FieldValue::owned_any(connection))) }) @@ -242,7 +241,7 @@ impl EntityObjectViaRelationBuilder { ), }; - match via_relation_definition.is_owner { + match via_rel_def_is_owner { false => field, true => field .argument(InputValue::new( diff --git a/src/query/loader.rs b/src/query/loader.rs index 5275aa41..7e0dcb43 100644 --- a/src/query/loader.rs +++ b/src/query/loader.rs @@ -1,11 +1,13 @@ mod impl_traits; -use impl_traits::*; +pub(crate) mod loader_impl; + +use loader_impl::*; use sea_orm::{ sea_query::{Value, ValueTuple}, - Condition, EntityTrait, ExprTrait, ModelTrait, QueryFilter, + EntityTrait, QueryFilter, RelationDef, }; -use std::{collections::HashMap, hash::Hash, marker::PhantomData, sync::Arc}; +use std::{collections::HashMap, hash::Hash, marker::PhantomData}; use crate::apply_order; @@ -25,21 +27,14 @@ pub struct HashableGroupKey where T: EntityTrait, { - /// Foundation SQL statement pub stmt: sea_orm::Select, - /// Columns tuple - pub columns: Vec, - /// Extra `WHERE` condition - pub filters: Option, - /// Ordering + pub junction_fields: Vec, + pub rel_def: RelationDef, + pub via_def: Option, + pub filters: sea_orm::Condition, pub order_by: Vec<(T::Column, sea_orm::sea_query::Order)>, } -#[derive(Clone, Debug)] -pub struct HashableColumn(T::Column) -where - T: EntityTrait; - pub struct OneToManyLoader where T: EntityTrait, @@ -71,77 +66,34 @@ where async fn load( &self, - keys: &[KeyComplex], + groups: &[KeyComplex], ) -> Result, Self::Value>, Self::Error> { - let items: HashMap, Vec> = keys - .iter() - .cloned() - .map(|item: KeyComplex| { - ( - HashableGroupKey { - stmt: item.meta.stmt, - columns: item.meta.columns, - filters: item.meta.filters, - order_by: item.meta.order_by, - }, - item.key, - ) - }) - .fold( - HashMap::, Vec>::new(), - |mut acc: HashMap, Vec>, - cur: (HashableGroupKey, ValueTuple)| { - match acc.get_mut(&cur.0) { - Some(items) => { - items.push(cur.1); - } - None => { - acc.insert(cur.0, vec![cur.1]); - } - } - - acc - }, - ); - - let promises: HashMap, _> = items - .into_iter() - .map(|(key, values): (HashableGroupKey, Vec)| { - let cloned_key = key.clone(); - - let stmt = key.stmt; - - let condition = match key.filters { - Some(condition) => Condition::all().add(condition), - None => Condition::all(), - }; - let condition = apply_condition(condition, &key.columns, values); - let stmt = stmt.filter(condition); - - let stmt = apply_order(stmt, key.order_by); - - (cloned_key, stmt.all(&self.connection)) - }) - .collect(); + let groups = consolidate_groups(groups); let mut results: HashMap, Vec> = HashMap::new(); - for (key, promise) in promises.into_iter() { - let key = key as HashableGroupKey; - let result: Vec = promise.await.map_err(Arc::new)?; - for item in result.into_iter() { - let key = &KeyComplex:: { - key: collect_key(key.columns.iter().map(|col: &T::Column| item.get(*col))), - meta: key.clone(), - }; - match results.get_mut(key) { - Some(results) => { - results.push(item); - } - None => { - results.insert(key.clone(), vec![item]); - } - }; + for (group, keys) in groups { + let g = group.clone(); + let mut stmt = g.stmt; + stmt = stmt.filter(g.filters); + stmt = apply_order(stmt, g.order_by); + let models: HashMap> = loader_impl( + keys, + g.junction_fields, + stmt, + g.rel_def, + g.via_def, + &self.connection, + ) + .await?; + for (key, models) in models { + results.insert( + KeyComplex { + key, + meta: group.clone(), + }, + models, + ); } } @@ -180,70 +132,36 @@ where async fn load( &self, - keys: &[KeyComplex], + groups: &[KeyComplex], ) -> Result, Self::Value>, Self::Error> { - let items: HashMap, Vec> = keys - .iter() - .cloned() - .map(|item: KeyComplex| { - ( - HashableGroupKey { - stmt: item.meta.stmt, - columns: item.meta.columns, - filters: item.meta.filters, - order_by: item.meta.order_by, - }, - item.key, - ) - }) - .fold( - HashMap::, Vec>::new(), - |mut acc: HashMap, Vec>, - cur: (HashableGroupKey, ValueTuple)| { - match acc.get_mut(&cur.0) { - Some(items) => { - items.push(cur.1); - } - None => { - acc.insert(cur.0, vec![cur.1]); - } - } - - acc - }, - ); - - let promises: HashMap, _> = items - .into_iter() - .map(|(key, values): (HashableGroupKey, Vec)| { - let cloned_key = key.clone(); - - let stmt = key.stmt; - - let condition = match key.filters { - Some(condition) => Condition::all().add(condition), - None => Condition::all(), - }; - let condition = apply_condition(condition, &key.columns, values); - let stmt = stmt.filter(condition); - - let stmt = apply_order(stmt, key.order_by); - - (cloned_key, stmt.all(&self.connection)) - }) - .collect(); + let groups = consolidate_groups(groups); let mut results: HashMap, T::Model> = HashMap::new(); - for (key, promise) in promises.into_iter() { - let key = key as HashableGroupKey; - let result: Vec = promise.await.map_err(Arc::new)?; - for item in result.into_iter() { - let key = &KeyComplex:: { - key: collect_key(key.columns.iter().map(|col: &T::Column| item.get(*col))), - meta: key.clone(), - }; - results.insert(key.clone(), item); + for (group, keys) in groups { + let g = group.clone(); + let mut stmt = g.stmt; + stmt = stmt.filter(g.filters); + stmt = apply_order(stmt, g.order_by); + let models: HashMap> = loader_impl( + keys, + g.junction_fields, + stmt, + g.rel_def, + g.via_def, + &self.connection, + ) + .await?; + for (key, model) in models { + if let Some(model) = model { + results.insert( + KeyComplex { + key, + meta: group.clone(), + }, + model, + ); + } } } @@ -251,27 +169,21 @@ where } } -fn apply_condition( - condition: Condition, - cols: &[T], - values: Vec, -) -> Condition { - if cols.len() == 1 { - condition.add( - sea_orm::sea_query::Expr::col(cols[0]).is_in(values.into_iter().map( - |tuple| match tuple { - ValueTuple::One(v) => v, - ValueTuple::Two(v, _) => v, - ValueTuple::Three(v, _, _) => v, - _ => panic!("Column & Value arity mismatch: expected 1"), - }, - )), - ) - } else { - let tuple = sea_orm::sea_query::Expr::tuple( - cols.iter() - .map(|column| sea_orm::sea_query::Expr::col(*column)), - ); - condition.add(tuple.in_tuples(values)) +fn consolidate_groups( + groups: &[KeyComplex], +) -> HashMap, Vec> { + let mut acc: HashMap, Vec> = Default::default(); + + for cur in groups { + match acc.get_mut(&cur.meta) { + Some(items) => { + items.push(cur.key.clone()); + } + None => { + acc.insert(cur.meta.clone(), vec![cur.key.clone()]); + } + } } + + acc } diff --git a/src/query/loader/impl_traits.rs b/src/query/loader/impl_traits.rs index 77da05b5..7f8867f7 100644 --- a/src/query/loader/impl_traits.rs +++ b/src/query/loader/impl_traits.rs @@ -87,11 +87,9 @@ where T: sea_orm::EntityTrait, { fn eq(&self, other: &Self) -> bool { - self.filters.eq(&other.filters) - && std::cmp::PartialEq::eq( - &format!("{:?}", self.columns), - &format!("{:?}", other.columns), - ) + self.rel_def.eq(&other.rel_def) + && self.via_def.eq(&other.via_def) + && self.filters.eq(&other.filters) && std::cmp::PartialEq::eq( &format!("{:?}", self.order_by), &format!("{:?}", other.order_by), @@ -106,33 +104,14 @@ where T: sea_orm::EntityTrait, { fn hash(&self, state: &mut H) { + self.rel_def.hash(state); + self.via_def.hash(state); format!("{:?}", self.filters).hash(state); - format!("{:?}", self.columns).hash(state); format!("{:?}", self.order_by).hash(state); } } -impl PartialEq for HashableColumn -where - T: sea_orm::EntityTrait, -{ - fn eq(&self, other: &Self) -> bool { - std::cmp::PartialEq::eq(&format!("{:?}", self.0), &format!("{:?}", other.0)) - } -} - -impl Eq for HashableColumn where T: sea_orm::EntityTrait {} - -impl Hash for HashableColumn -where - T: sea_orm::EntityTrait, -{ - fn hash(&self, state: &mut H) { - format!("{:?}", self.0).hash(state); - } -} - -pub struct ValueTupleIter<'a> { +struct ValueTupleIter<'a> { key: &'a ValueTuple, index: usize, } @@ -173,21 +152,3 @@ impl<'a> Iterator for ValueTupleIter<'a> { result } } - -pub fn collect_key(mut it: impl Iterator) -> ValueTuple { - match (it.next(), it.next(), it.next(), it.next()) { - (Some(a), None, _, _) => ValueTuple::One(a), - (Some(a), Some(b), None, _) => ValueTuple::Two(a, b), - (Some(a), Some(b), Some(c), None) => ValueTuple::Three(a, b, c), - (Some(a), Some(b), Some(c), Some(d)) => { - // collect remaining into vec - let mut v = vec![a, b, c, d]; - v.extend(it); - ValueTuple::Many(v) - } - (None, _, _, _) => { - // empty iterator - ValueTuple::Many(Vec::new()) - } - } -} diff --git a/src/query/loader/loader_impl.rs b/src/query/loader/loader_impl.rs new file mode 100644 index 00000000..f7917d22 --- /dev/null +++ b/src/query/loader/loader_impl.rs @@ -0,0 +1,300 @@ +//! Mostly copied from SeaORM +use sea_orm::{ + dynamic, + sea_query::{ColumnRef, DynIden, Expr, ExprTrait, IntoColumnRef, TableRef, ValueTuple}, + Condition, ConnectionTrait, DbErr, EntityTrait, Identity, JoinType, ModelTrait, QueryFilter, + QuerySelect, RelationDef, Select, +}; +use std::{collections::HashMap, str::FromStr}; + +pub trait Container: Default + Clone { + type Item; + fn add(&mut self, item: Self::Item); +} + +impl Container for Vec { + type Item = T; + fn add(&mut self, item: Self::Item) { + self.push(item); + } +} + +impl Container for Option { + type Item = T; + fn add(&mut self, item: Self::Item) { + self.replace(item); + } +} + +pub(super) async fn loader_impl( + keys: Vec, + junction_fields: Vec, + stmt: Select, + rel_def: RelationDef, + via_def: Option, + db: &C, +) -> Result, DbErr> +where + C: ConnectionTrait, + R: EntityTrait, + R::Model: Send + Sync, + T: Container, +{ + if keys.is_empty() { + return Ok(Default::default()); + } + + if let Some(via_def) = via_def { + let condition = prepare_condition(&via_def.to_tbl, &via_def.to_col, &keys)?; + + let stmt = QueryFilter::filter(stmt.join_rev(JoinType::InnerJoin, rel_def), condition); + + // The idea is to do a SelectTwo with join, then extract key via a dynamic model + // i.e. select (baker + cake_baker) and extract cake_id from result rows + // SELECT "baker"."id", "baker"."name", "baker"."contact_details", "baker"."bakery_id", + // "cakes_bakers"."cake_id" <- extra select + // FROM "baker" <- target + // INNER JOIN "cakes_bakers" <- junction + // ON "cakes_bakers"."baker_id" = "baker"."id" <- relation + // WHERE "cakes_bakers"."cake_id" IN (..) + + let data = stmt + .select_also_dyn_model( + via_def.to_tbl.sea_orm_table().clone(), + dynamic::ModelType { + fields: junction_fields, + }, + ) + .all(db) + .await?; + + let mut hashmap: HashMap = + keys.iter() + .fold(HashMap::new(), |mut acc, key: &ValueTuple| { + acc.insert(key.clone(), T::default()); + acc + }); + + for (item, key) in data { + let key = dyn_model_to_key(key)?; + + let vec = hashmap.get_mut(&key).ok_or_else(|| { + DbErr::RecordNotFound(format!("Loader: failed to find model for {key:?}")) + })?; + + vec.add(item); + } + + Ok(hashmap) + } else { + let condition = prepare_condition(&rel_def.to_tbl, &rel_def.to_col, &keys)?; + + let stmt = QueryFilter::filter(stmt, condition); + + let data = stmt.all(db).await?; + + let mut hashmap: HashMap = Default::default(); + + for item in data { + let key = extract_key(&rel_def.to_col, &item)?; + let holder = hashmap.entry(key).or_default(); + holder.add(item); + } + + Ok(hashmap) + } +} + +pub(crate) fn extract_key(target_col: &Identity, model: &Model) -> Result +where + Model: ModelTrait, +{ + let values = IdenIter::new(target_col) + .map(|col| { + let col_name = col.inner(); + let column = + <<::Entity as EntityTrait>::Column as FromStr>::from_str( + &col_name, + ) + .map_err(|_| DbErr::Type(format!("Failed at mapping '{col_name}' to column")))?; + Ok(model.get(column)) + }) + .collect::, DbErr>>()?; + + Ok(match values.len() { + 0 => return Err(DbErr::Type("Identity zero?".into())), + 1 => ValueTuple::One(values.into_iter().next().expect("checked")), + 2 => { + let mut it = values.into_iter(); + ValueTuple::Two(it.next().expect("checked"), it.next().expect("checked")) + } + 3 => { + let mut it = values.into_iter(); + ValueTuple::Three( + it.next().expect("checked"), + it.next().expect("checked"), + it.next().expect("checked"), + ) + } + _ => ValueTuple::Many(values), + }) +} + +pub(crate) fn extract_col_type( + left: &Identity, + right: &Identity, +) -> Result, DbErr> +where + Model: ModelTrait, +{ + use itertools::Itertools; + + if left.arity() != right.arity() { + return Err(DbErr::Type(format!( + "Identity mismatch: left: {} != right: {}", + left.arity(), + right.arity() + ))); + } + + let vec = IdenIter::new(left) + .zip_eq(IdenIter::new(right)) + .map(|(l, r)| { + let col_a = + <<::Entity as EntityTrait>::Column as FromStr>::from_str( + &l.inner(), + ) + .map_err(|_| DbErr::Type(format!("Failed at mapping '{l}'")))?; + Ok(dynamic::FieldType::new( + r.clone(), + Model::get_value_type(col_a), + )) + }) + .collect::, DbErr>>()?; + + Ok(vec) +} + +#[allow(clippy::unwrap_used)] +fn dyn_model_to_key(dyn_model: dynamic::Model) -> Result { + Ok(match dyn_model.fields.len() { + 0 => return Err(DbErr::Type("Identity zero?".into())), + 1 => ValueTuple::One(dyn_model.fields.into_iter().next().unwrap().value), + 2 => { + let mut iter = dyn_model.fields.into_iter(); + ValueTuple::Two(iter.next().unwrap().value, iter.next().unwrap().value) + } + 3 => { + let mut iter = dyn_model.fields.into_iter(); + ValueTuple::Three( + iter.next().unwrap().value, + iter.next().unwrap().value, + iter.next().unwrap().value, + ) + } + _ => ValueTuple::Many(dyn_model.fields.into_iter().map(|v| v.value).collect()), + }) +} + +fn arity_mismatch(expected: usize, actual: &ValueTuple) -> DbErr { + DbErr::Type(format!( + "Loader: arity mismatch: expected {expected}, got {} in {actual:?}", + actual.arity() + )) +} + +fn prepare_condition( + table: &TableRef, + to: &Identity, + keys: &[ValueTuple], +) -> Result { + use itertools::Itertools; + + let arity = to.arity(); + let keys = keys.iter().unique(); + + let expr = if arity == 1 { + let values = keys + .map(|key| match key { + ValueTuple::One(v) => Ok(Expr::val(v.to_owned())), + _ => Err(arity_mismatch(arity, key)), + }) + .collect::, DbErr>>()?; + + Expr::col(table_column(table, IdenIter::new(to).next().unwrap())).is_in(values) + } else { + let table_columns = create_table_columns(table, to); + + // A vector of tuples of values, e.g. [(v11, v12, ...), (v21, v22, ...), ...] + let value_tuples = keys + .map(|key| { + let key_arity = key.arity(); + if arity != key_arity { + return Err(arity_mismatch(arity, key)); + } + + let tuple_exprs = key.clone().into_iter().map(Expr::val); + + Ok(Expr::tuple(tuple_exprs)) + }) + .collect::, DbErr>>()?; + + // Build `(c1, c2, ...) IN ((v11, v12, ...), (v21, v22, ...), ...)` + Expr::tuple(table_columns).is_in(value_tuples) + }; + + Ok(expr.into()) +} + +fn table_column(tbl: &TableRef, col: &DynIden) -> ColumnRef { + (tbl.sea_orm_table().to_owned(), col.clone()).into_column_ref() +} + +/// Create a vector of `Expr::col` from the table and identity, e.g. [Expr::col((table, col1)), Expr::col((table, col2)), ...] +fn create_table_columns(table: &TableRef, cols: &Identity) -> Vec { + IdenIter::new(cols) + .map(|col| table_column(table, col)) + .map(Expr::col) + .collect() +} + +struct IdenIter<'a> { + identity: &'a Identity, + index: usize, +} + +impl<'a> IdenIter<'a> { + fn new(identity: &'a Identity) -> Self { + Self { identity, index: 0 } + } +} + +impl<'a> Iterator for IdenIter<'a> { + type Item = &'a DynIden; + + fn next(&mut self) -> Option { + let result = match self.identity { + Identity::Unary(iden1) => { + if self.index == 0 { + Some(iden1) + } else { + None + } + } + Identity::Binary(iden1, iden2) => match self.index { + 0 => Some(iden1), + 1 => Some(iden2), + _ => None, + }, + Identity::Ternary(iden1, iden2, iden3) => match self.index { + 0 => Some(iden1), + 1 => Some(iden2), + 2 => Some(iden3), + _ => None, + }, + Identity::Many(vec) => vec.get(self.index), + }; + self.index += 1; + result + } +} From 3d0c545a9428a5b1982701e1ed3aa7301084a72b Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Fri, 24 Oct 2025 22:21:30 +0100 Subject: [PATCH 49/85] Add one more staff --- examples/sqlite/sakila-data.sql | 5 +++ examples/sqlite/sakila.db | Bin 5824512 -> 5824512 bytes examples/sqlite/tests/custom_query_tests.rs | 4 +- examples/sqlite/tests/hook_guard_tests.rs | 6 +-- examples/sqlite/tests/plural_query_tests.rs | 41 +++++++++++++------- examples/sqlite/tests/query_tests.rs | 37 +++++++++++------- sakila.db | Bin 5824512 -> 5824512 bytes 7 files changed, 58 insertions(+), 35 deletions(-) diff --git a/examples/sqlite/sakila-data.sql b/examples/sqlite/sakila-data.sql index 46c61fb9..dbeedf93 100644 --- a/examples/sqlite/sakila-data.sql +++ b/examples/sqlite/sakila-data.sql @@ -7673,6 +7673,11 @@ Insert into staff Values ('2','Jon','Stephens','4','1',NULL,'Jon.Stephens@sakilastaff.com','2','1','Jon','8cb2237d0679ca88db6464eac60da96345513964','2006-02-15 04:57:16.000') ; +Insert into staff + (staff_id,first_name,last_name,address_id,reports_to_id,picture,email,store_id,active,username,password,last_update) +Values + (3,'Emily','Clarke','16','1',NULL,'Emily.Clarke@sakilastaff.com','2','1','Emily','8cb2237d0679ca88db6464eac60da96345513964','2006-02-15 05:38:22.000') +; -- Automatically generated by Advanced ETl Processor -- http://www.etl-tools.com/ -- table store diff --git a/examples/sqlite/sakila.db b/examples/sqlite/sakila.db index e511f1975e04405cb48deacc3f70060ef3e47c92..373d8cb5a574c61e14058b840dad9aedc9e1bccd 100644 GIT binary patch delta 599 zcmZ|IOHW!+5Ww+!&k^vVaxPM(zNzmQC> ze|G7((A24NIWE%9jJz_^o{Y>Cjqb@Z-Cd;{T0}S8Kl@~4dn>sgPR4#GHq1iqdo9Ly zV~MR~Z1+!WbJO>0XIm7Sspz%nGDBW}uQw3%1pGrhe=y~`7Wa0ka+-Ql4=VR|u-|I6 zNL`mQ{qSEn%y(w$UuJ|X{6b2@uxpwoRaR4vDzCC{>q4*VOnIEo3cSQ3SYU+>#V}w; z2})502NX;=Q4SYApaPX}<0Gn2jT+RV4)th&2cOW0CN!f3t!P6#I?#zObi)fDde93$ z0_Z~!A@pMapD~Cbe8DhAFp4pZV*+0h#y3Rp9g~>CG-fc1Im}}LQ7mE!%UHoG*07$= JXMdFBo&j1BDKP*5 delta 445 zcmYMqxlRI606@{<&CCPb-r%@{isHWDzKfu^;l6>fpU<$s!kAcTMX4WPVue%6Z`hIe z0boHvwD4M4oa7caTlf@NJOn-RF_bi=7L9-0IJb@`?}+CopHQ}@?v?EZiu#M+4Hxw* zQ(S8~`&E494gE*G6@+0?!jctDeXGj<8I!s4yD;vmB>^3OZ9>8eAN;66E$UE@1}Inv zpb<8j(2N$eq7CinKoB8xq6^&!qX)f+pbz~Rz#xV&j1i1t4C9Dm0+X1+G-fc1Im}}L zi&(-kR)1dNo7loOcCd>S_OOow9O4MaIKe5-aE=S4k#Vb~OK Schema { } fn assert_eq(a: Response, b: &str) { + let json = a.data.into_json().unwrap(); + println!("{}", serde_json::to_string(&json).unwrap()); assert_eq!( - a.data.into_json().unwrap(), + json, serde_json::from_str::(b).unwrap() ) } @@ -470,34 +472,43 @@ async fn test_self_ref() { .await, r#" { - "staff": { + "staff": { "nodes": [ - { + { "firstName": "Mike", "reportsToId": null, "selfRefReverse": { - "nodes": [ - { - "staffId": 2, - "firstName": "Jon" - } - ] + "nodes": [ + {"staffId": 2, "firstName": "Jon"}, + {"staffId": 3, "firstName": "Emily"} + ] }, "selfRef": null - }, - { + }, + { "firstName": "Jon", "reportsToId": 1, "selfRefReverse": { - "nodes": [] + "nodes": [] }, "selfRef": { - "staffId": 1, - "firstName": "Mike" + "staffId": 1, + "firstName": "Mike" } + }, + { + "firstName": "Emily", + "reportsToId": 1, + "selfRefReverse": { + "nodes": [] + }, + "selfRef": { + "staffId": 1, + "firstName": "Mike" } + } ] - } + } } "#, ) diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index e7a46ac7..cf3b872a 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -569,34 +569,43 @@ async fn test_self_ref() { .await, r#" { - "staff": { + "staff": { "nodes": [ - { + { "firstName": "Mike", "reportsToId": null, "selfRefReverse": { - "nodes": [ - { - "staffId": 2, - "firstName": "Jon" - } - ] + "nodes": [ + {"staffId": 2, "firstName": "Jon"}, + {"staffId": 3, "firstName": "Emily"} + ] }, "selfRef": null - }, - { + }, + { "firstName": "Jon", "reportsToId": 1, "selfRefReverse": { - "nodes": [] + "nodes": [] }, "selfRef": { - "staffId": 1, - "firstName": "Mike" + "staffId": 1, + "firstName": "Mike" } + }, + { + "firstName": "Emily", + "reportsToId": 1, + "selfRefReverse": { + "nodes": [] + }, + "selfRef": { + "staffId": 1, + "firstName": "Mike" } + } ] - } + } } "#, ) diff --git a/sakila.db b/sakila.db index e511f1975e04405cb48deacc3f70060ef3e47c92..373d8cb5a574c61e14058b840dad9aedc9e1bccd 100644 GIT binary patch delta 599 zcmZ|IOHW!+5Ww+!&k^vVaxPM(zNzmQC> ze|G7((A24NIWE%9jJz_^o{Y>Cjqb@Z-Cd;{T0}S8Kl@~4dn>sgPR4#GHq1iqdo9Ly zV~MR~Z1+!WbJO>0XIm7Sspz%nGDBW}uQw3%1pGrhe=y~`7Wa0ka+-Ql4=VR|u-|I6 zNL`mQ{qSEn%y(w$UuJ|X{6b2@uxpwoRaR4vDzCC{>q4*VOnIEo3cSQ3SYU+>#V}w; z2})502NX;=Q4SYApaPX}<0Gn2jT+RV4)th&2cOW0CN!f3t!P6#I?#zObi)fDde93$ z0_Z~!A@pMapD~Cbe8DhAFp4pZV*+0h#y3Rp9g~>CG-fc1Im}}LQ7mE!%UHoG*07$= JXMdFBo&j1BDKP*5 delta 445 zcmYMqxlRI606@{<&CCPb-r%@{isHWDzKfu^;l6>fpU<$s!kAcTMX4WPVue%6Z`hIe z0boHvwD4M4oa7caTlf@NJOn-RF_bi=7L9-0IJb@`?}+CopHQ}@?v?EZiu#M+4Hxw* zQ(S8~`&E494gE*G6@+0?!jctDeXGj<8I!s4yD;vmB>^3OZ9>8eAN;66E$UE@1}Inv zpb<8j(2N$eq7CinKoB8xq6^&!qX)f+pbz~Rz#xV&j1i1t4C9Dm0+X1+G-fc1Im}}L zi&(-kR)1dNo7loOcCd>S_OOow9O4MaIKe5-aE=S4k#Vb~OK Date: Sat, 25 Oct 2025 01:01:14 +0100 Subject: [PATCH 50/85] Update SQLite example to use dense entity --- examples/sqlite/src/entities/actor.rs | 34 +------ examples/sqlite/src/entities/address.rs | 63 +++---------- examples/sqlite/src/entities/category.rs | 34 +------ examples/sqlite/src/entities/city.rs | 39 ++------ examples/sqlite/src/entities/country.rs | 23 +---- examples/sqlite/src/entities/customer.rs | 67 +++----------- examples/sqlite/src/entities/film.rs | 91 ++++--------------- examples/sqlite/src/entities/film_actor.rs | 43 +++------ examples/sqlite/src/entities/film_category.rs | 43 +++------ examples/sqlite/src/entities/film_text.rs | 9 +- examples/sqlite/src/entities/inventory.rs | 55 +++-------- examples/sqlite/src/entities/language.rs | 9 +- examples/sqlite/src/entities/mod.rs | 2 + examples/sqlite/src/entities/payment.rs | 59 +++--------- examples/sqlite/src/entities/prelude.rs | 2 + examples/sqlite/src/entities/rental.rs | 71 ++++----------- examples/sqlite/src/entities/staff.rs | 80 ++++------------ examples/sqlite/src/entities/store.rs | 67 +++----------- 18 files changed, 183 insertions(+), 608 deletions(-) diff --git a/examples/sqlite/src/entities/actor.rs b/examples/sqlite/src/entities/actor.rs index 3dc2de64..6a90adec 100644 --- a/examples/sqlite/src/entities/actor.rs +++ b/examples/sqlite/src/entities/actor.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "actor")] pub struct Model { @@ -8,35 +11,8 @@ pub struct Model { pub first_name: String, pub last_name: String, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::film_actor::Entity")] - FilmActor, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::FilmActor.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::film_actor::Relation::Film.def() - } - fn via() -> Option { - Some(super::film_actor::Relation::Actor.def().rev()) - } + #[sea_orm(has_many, via = "film_actor")] + pub films: HasMany, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::film_actor::Entity")] - FilmActor, - #[sea_orm(entity = "super::film::Entity")] - Film, -} diff --git a/examples/sqlite/src/entities/address.rs b/examples/sqlite/src/entities/address.rs index 60099741..957e074a 100644 --- a/examples/sqlite/src/entities/address.rs +++ b/examples/sqlite/src/entities/address.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "address")] pub struct Model { @@ -12,60 +15,20 @@ pub struct Model { pub postal_code: Option, pub phone: String, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::city::Entity", - from = "Column::CityId", - to = "super::city::Column::CityId", + belongs_to, + from = "city_id", + to = "city_id", on_update = "Cascade", on_delete = "NoAction" )] - City, - #[sea_orm(has_many = "super::customer::Entity")] - Customer, - #[sea_orm(has_many = "super::staff::Entity")] - Staff, - #[sea_orm(has_many = "super::store::Entity")] - Store, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::City.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Customer.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Staff.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Store.def() - } + pub city: HasOne, + #[sea_orm(has_many)] + pub customers: HasMany, + #[sea_orm(has_many)] + pub staff: HasMany, + #[sea_orm(has_many)] + pub stores: HasMany, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::city::Entity")] - City, - #[sea_orm(entity = "super::customer::Entity")] - Customer, - #[sea_orm(entity = "super::staff::Entity")] - Staff, - #[sea_orm(entity = "super::store::Entity")] - Store, -} diff --git a/examples/sqlite/src/entities/category.rs b/examples/sqlite/src/entities/category.rs index a3b74463..85b37509 100644 --- a/examples/sqlite/src/entities/category.rs +++ b/examples/sqlite/src/entities/category.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "category")] pub struct Model { @@ -7,35 +10,8 @@ pub struct Model { pub category_id: i16, pub name: String, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::film_category::Entity")] - FilmCategory, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::FilmCategory.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::film_category::Relation::Film.def() - } - fn via() -> Option { - Some(super::film_category::Relation::Category.def().rev()) - } + #[sea_orm(has_many, via = "film_category")] + pub films: HasMany, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::film_category::Entity")] - FilmCategory, - #[sea_orm(entity = "super::film::Entity")] - Film, -} diff --git a/examples/sqlite/src/entities/city.rs b/examples/sqlite/src/entities/city.rs index 0e2ac4ce..03524d62 100644 --- a/examples/sqlite/src/entities/city.rs +++ b/examples/sqlite/src/entities/city.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "city")] pub struct Model { @@ -8,40 +11,16 @@ pub struct Model { pub city: String, pub country_id: i16, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::address::Entity")] - Address, + #[sea_orm(has_many)] + pub addresses: HasMany, #[sea_orm( - belongs_to = "super::country::Entity", - from = "Column::CountryId", - to = "super::country::Column::CountryId", + belongs_to, + from = "country_id", + to = "country_id", on_update = "Cascade", on_delete = "NoAction" )] - Country, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Address.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Country.def() - } + pub country: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::address::Entity")] - Address, - #[sea_orm(entity = "super::country::Entity")] - Country, -} diff --git a/examples/sqlite/src/entities/country.rs b/examples/sqlite/src/entities/country.rs index 2a2c442f..16a68349 100644 --- a/examples/sqlite/src/entities/country.rs +++ b/examples/sqlite/src/entities/country.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "country")] pub struct Model { @@ -7,24 +10,8 @@ pub struct Model { pub country_id: i16, pub country: String, pub last_update: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::city::Entity")] - City, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::City.def() - } + #[sea_orm(has_many)] + pub cities: HasMany, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::city::Entity")] - City, -} diff --git a/examples/sqlite/src/entities/customer.rs b/examples/sqlite/src/entities/customer.rs index a8cd80a8..61ca4f45 100644 --- a/examples/sqlite/src/entities/customer.rs +++ b/examples/sqlite/src/entities/customer.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "customer")] pub struct Model { @@ -13,66 +16,26 @@ pub struct Model { pub active: i16, pub create_date: DateTimeUtc, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::address::Entity", - from = "Column::AddressId", - to = "super::address::Column::AddressId", + belongs_to, + from = "address_id", + to = "address_id", on_update = "Cascade", on_delete = "NoAction" )] - Address, - #[sea_orm(has_many = "super::payment::Entity")] - Payment, - #[sea_orm(has_many = "super::rental::Entity")] - Rental, + pub address: HasOne, + #[sea_orm(has_many)] + pub payments: HasMany, + #[sea_orm(has_many)] + pub rentals: HasMany, #[sea_orm( - belongs_to = "super::store::Entity", - from = "Column::StoreId", - to = "super::store::Column::StoreId", + belongs_to, + from = "store_id", + to = "store_id", on_update = "Cascade", on_delete = "NoAction" )] - Store, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Address.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Payment.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Rental.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Store.def() - } + pub store: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::address::Entity")] - Address, - #[sea_orm(entity = "super::payment::Entity")] - Payment, - #[sea_orm(entity = "super::rental::Entity")] - Rental, - #[sea_orm(entity = "super::store::Entity")] - Store, -} diff --git a/examples/sqlite/src/entities/film.rs b/examples/sqlite/src/entities/film.rs index b2521501..224146f0 100644 --- a/examples/sqlite/src/entities/film.rs +++ b/examples/sqlite/src/entities/film.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "film")] pub struct Model { @@ -20,86 +23,30 @@ pub struct Model { pub rating: Option, pub special_features: Option, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::film_actor::Entity")] - FilmActor, - #[sea_orm(has_many = "super::film_category::Entity")] - FilmCategory, - #[sea_orm(has_many = "super::inventory::Entity")] - Inventory, + #[sea_orm(has_many)] + pub inventories: HasMany, #[sea_orm( - belongs_to = "super::language::Entity", - from = "Column::OriginalLanguageId", - to = "super::language::Column::LanguageId", + belongs_to, + relation_enum = "Language2", + from = "original_language_id", + to = "language_id", on_update = "NoAction", on_delete = "NoAction" )] - Language2, + pub language_2: HasOne, #[sea_orm( - belongs_to = "super::language::Entity", - from = "Column::LanguageId", - to = "super::language::Column::LanguageId", + belongs_to, + relation_enum = "Language1", + from = "language_id", + to = "language_id", on_update = "NoAction", on_delete = "NoAction" )] - Language1, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::FilmActor.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::FilmCategory.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Inventory.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::film_actor::Relation::Actor.def() - } - fn via() -> Option { - Some(super::film_actor::Relation::Film.def().rev()) - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::film_category::Relation::Category.def() - } - fn via() -> Option { - Some(super::film_category::Relation::Film.def().rev()) - } + pub language_1: HasOne, + #[sea_orm(has_many, via = "film_actor")] + pub actors: HasMany, + #[sea_orm(has_many, via = "film_category")] + pub categories: HasMany, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::film_actor::Entity")] - FilmActor, - #[sea_orm(entity = "super::film_category::Entity")] - FilmCategory, - #[sea_orm(entity = "super::inventory::Entity")] - Inventory, - #[sea_orm(entity = "super::language::Entity", def = "Relation::Language2.def()")] - Language2, - #[sea_orm(entity = "super::language::Entity", def = "Relation::Language1.def()")] - Language1, - #[sea_orm(entity = "super::actor::Entity")] - Actor, - #[sea_orm(entity = "super::category::Entity")] - Category, -} diff --git a/examples/sqlite/src/entities/film_actor.rs b/examples/sqlite/src/entities/film_actor.rs index 66c401e2..dcdc6f08 100644 --- a/examples/sqlite/src/entities/film_actor.rs +++ b/examples/sqlite/src/entities/film_actor.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "film_actor")] pub struct Model { @@ -8,46 +11,22 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub film_id: i32, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::actor::Entity", - from = "Column::ActorId", - to = "super::actor::Column::ActorId", + belongs_to, + from = "actor_id", + to = "actor_id", on_update = "Cascade", on_delete = "NoAction" )] - Actor, + pub actor: HasOne, #[sea_orm( - belongs_to = "super::film::Entity", - from = "Column::FilmId", - to = "super::film::Column::FilmId", + belongs_to, + from = "film_id", + to = "film_id", on_update = "Cascade", on_delete = "NoAction" )] - Film, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Actor.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Film.def() - } + pub film: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::actor::Entity")] - Actor, - #[sea_orm(entity = "super::film::Entity")] - Film, -} diff --git a/examples/sqlite/src/entities/film_category.rs b/examples/sqlite/src/entities/film_category.rs index 7dbc2fe1..50c16a3b 100644 --- a/examples/sqlite/src/entities/film_category.rs +++ b/examples/sqlite/src/entities/film_category.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "film_category")] pub struct Model { @@ -8,46 +11,22 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub category_id: i16, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::category::Entity", - from = "Column::CategoryId", - to = "super::category::Column::CategoryId", + belongs_to, + from = "category_id", + to = "category_id", on_update = "Cascade", on_delete = "NoAction" )] - Category, + pub category: HasOne, #[sea_orm( - belongs_to = "super::film::Entity", - from = "Column::FilmId", - to = "super::film::Column::FilmId", + belongs_to, + from = "film_id", + to = "film_id", on_update = "Cascade", on_delete = "NoAction" )] - Film, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Category.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Film.def() - } + pub film: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::category::Entity")] - Category, - #[sea_orm(entity = "super::film::Entity")] - Film, -} diff --git a/examples/sqlite/src/entities/film_text.rs b/examples/sqlite/src/entities/film_text.rs index 2a09460f..bf7525c4 100644 --- a/examples/sqlite/src/entities/film_text.rs +++ b/examples/sqlite/src/entities/film_text.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "film_text")] pub struct Model { @@ -10,10 +13,4 @@ pub struct Model { pub description: Option, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity {} diff --git a/examples/sqlite/src/entities/inventory.rs b/examples/sqlite/src/entities/inventory.rs index 97068295..5b9fa16d 100644 --- a/examples/sqlite/src/entities/inventory.rs +++ b/examples/sqlite/src/entities/inventory.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "inventory")] pub struct Model { @@ -8,56 +11,24 @@ pub struct Model { pub film_id: i32, pub store_id: i32, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::film::Entity", - from = "Column::FilmId", - to = "super::film::Column::FilmId", + belongs_to, + from = "film_id", + to = "film_id", on_update = "Cascade", on_delete = "NoAction" )] - Film, - #[sea_orm(has_many = "super::rental::Entity")] - Rental, + pub film: HasOne, + #[sea_orm(has_many)] + pub rentals: HasMany, #[sea_orm( - belongs_to = "super::store::Entity", - from = "Column::StoreId", - to = "super::store::Column::StoreId", + belongs_to, + from = "store_id", + to = "store_id", on_update = "Cascade", on_delete = "NoAction" )] - Store, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Film.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Rental.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Store.def() - } + pub store: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::film::Entity")] - Film, - #[sea_orm(entity = "super::rental::Entity")] - Rental, - #[sea_orm(entity = "super::store::Entity")] - Store, -} diff --git a/examples/sqlite/src/entities/language.rs b/examples/sqlite/src/entities/language.rs index 0e906e9b..672ae22d 100644 --- a/examples/sqlite/src/entities/language.rs +++ b/examples/sqlite/src/entities/language.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "language")] pub struct Model { @@ -9,10 +12,4 @@ pub struct Model { pub last_update: DateTimeUtc, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity {} diff --git a/examples/sqlite/src/entities/mod.rs b/examples/sqlite/src/entities/mod.rs index 59213d1d..0203059c 100644 --- a/examples/sqlite/src/entities/mod.rs +++ b/examples/sqlite/src/entities/mod.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + pub mod prelude; pub mod actor; diff --git a/examples/sqlite/src/entities/payment.rs b/examples/sqlite/src/entities/payment.rs index 26c1aee1..ff66c63c 100644 --- a/examples/sqlite/src/entities/payment.rs +++ b/examples/sqlite/src/entities/payment.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "payment")] pub struct Model { @@ -12,62 +15,30 @@ pub struct Model { pub amount: Decimal, pub payment_date: DateTimeUtc, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::customer::Entity", - from = "Column::CustomerId", - to = "super::customer::Column::CustomerId", + belongs_to, + from = "customer_id", + to = "customer_id", on_update = "NoAction", on_delete = "NoAction" )] - Customer, + pub customer: HasOne, #[sea_orm( - belongs_to = "super::rental::Entity", - from = "Column::RentalId", - to = "super::rental::Column::RentalId", + belongs_to, + from = "rental_id", + to = "rental_id", on_update = "Cascade", on_delete = "SetNull" )] - Rental, + pub rental: HasOne, #[sea_orm( - belongs_to = "super::staff::Entity", - from = "Column::StaffId", - to = "super::staff::Column::StaffId", + belongs_to, + from = "staff_id", + to = "staff_id", on_update = "NoAction", on_delete = "NoAction" )] - Staff, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Customer.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Rental.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Staff.def() - } + pub staff: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::customer::Entity")] - Customer, - #[sea_orm(entity = "super::rental::Entity")] - Rental, - #[sea_orm(entity = "super::staff::Entity")] - Staff, -} diff --git a/examples/sqlite/src/entities/prelude.rs b/examples/sqlite/src/entities/prelude.rs index 07d114ee..74e18682 100644 --- a/examples/sqlite/src/entities/prelude.rs +++ b/examples/sqlite/src/entities/prelude.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + pub use super::actor::Entity as Actor; pub use super::address::Entity as Address; pub use super::category::Entity as Category; diff --git a/examples/sqlite/src/entities/rental.rs b/examples/sqlite/src/entities/rental.rs index 58975216..7d64ea32 100644 --- a/examples/sqlite/src/entities/rental.rs +++ b/examples/sqlite/src/entities/rental.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "rental")] pub struct Model { @@ -11,72 +14,32 @@ pub struct Model { pub return_date: Option, pub staff_id: i16, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::customer::Entity", - from = "Column::CustomerId", - to = "super::customer::Column::CustomerId", + belongs_to, + from = "customer_id", + to = "customer_id", on_update = "NoAction", on_delete = "NoAction" )] - Customer, + pub customer: HasOne, #[sea_orm( - belongs_to = "super::inventory::Entity", - from = "Column::InventoryId", - to = "super::inventory::Column::InventoryId", + belongs_to, + from = "inventory_id", + to = "inventory_id", on_update = "NoAction", on_delete = "NoAction" )] - Inventory, - #[sea_orm(has_many = "super::payment::Entity")] - Payment, + pub inventory: HasOne, + #[sea_orm(has_many)] + pub payments: HasMany, #[sea_orm( - belongs_to = "super::staff::Entity", - from = "Column::StaffId", - to = "super::staff::Column::StaffId", + belongs_to, + from = "staff_id", + to = "staff_id", on_update = "NoAction", on_delete = "NoAction" )] - Staff, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Customer.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Inventory.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Payment.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Staff.def() - } + pub staff: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::customer::Entity")] - Customer, - #[sea_orm(entity = "super::inventory::Entity")] - Inventory, - #[sea_orm(entity = "super::payment::Entity")] - Payment, - #[sea_orm(entity = "super::staff::Entity")] - Staff, -} diff --git a/examples/sqlite/src/entities/staff.rs b/examples/sqlite/src/entities/staff.rs index 781654eb..02437093 100644 --- a/examples/sqlite/src/entities/staff.rs +++ b/examples/sqlite/src/entities/staff.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "staff")] pub struct Model { @@ -17,78 +20,35 @@ pub struct Model { pub username: String, pub password: Option, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::address::Entity", - from = "Column::AddressId", - to = "super::address::Column::AddressId", + belongs_to, + from = "address_id", + to = "address_id", on_update = "Cascade", on_delete = "NoAction" )] - Address, - #[sea_orm(has_many = "super::payment::Entity")] - Payment, - #[sea_orm(has_many = "super::rental::Entity")] - Rental, + pub address: HasOne, + #[sea_orm(has_many)] + pub payments: HasMany, + #[sea_orm(has_many)] + pub rentals: HasMany, #[sea_orm( - belongs_to = "Entity", - from = "Column::ReportsToId", - to = "Column::StaffId", + self_ref, + relation_enum = "SelfRef", + from = "reports_to_id", + to = "staff_id", on_update = "Cascade", on_delete = "NoAction" )] - SelfRef, + pub staff: HasOne, #[sea_orm( - belongs_to = "super::store::Entity", - from = "Column::StoreId", - to = "super::store::Column::StoreId", + belongs_to, + from = "store_id", + to = "store_id", on_update = "Cascade", on_delete = "NoAction" )] - Store, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Address.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Payment.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Rental.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Store.def() - } + pub store: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::address::Entity")] - Address, - #[sea_orm(entity = "super::payment::Entity")] - Payment, - #[sea_orm(entity = "super::rental::Entity")] - Rental, - #[sea_orm(entity = "Entity", def = "Relation::SelfRef.def()")] - SelfRef, - #[sea_orm(entity = "super::store::Entity")] - Store, - #[sea_orm(entity = "Entity", def = "Relation::SelfRef.def().rev()")] - SelfRefReverse, -} diff --git a/examples/sqlite/src/entities/store.rs b/examples/sqlite/src/entities/store.rs index f96f63c3..70e75017 100644 --- a/examples/sqlite/src/entities/store.rs +++ b/examples/sqlite/src/entities/store.rs @@ -1,5 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "store")] pub struct Model { @@ -8,66 +11,26 @@ pub struct Model { pub manager_staff_id: i16, pub address_id: i32, pub last_update: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { #[sea_orm( - belongs_to = "super::address::Entity", - from = "Column::AddressId", - to = "super::address::Column::AddressId", + belongs_to, + from = "address_id", + to = "address_id", on_update = "NoAction", on_delete = "NoAction" )] - Address, - #[sea_orm(has_many = "super::customer::Entity")] - Customer, - #[sea_orm(has_many = "super::inventory::Entity")] - Inventory, + pub address: HasOne, + #[sea_orm(has_many)] + pub customers: HasMany, + #[sea_orm(has_many)] + pub inventories: HasMany, #[sea_orm( - belongs_to = "super::staff::Entity", - from = "Column::ManagerStaffId", - to = "super::staff::Column::StaffId", + belongs_to, + from = "manager_staff_id", + to = "staff_id", on_update = "NoAction", on_delete = "NoAction" )] - Staff, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Address.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Customer.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Inventory.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Staff.def() - } + pub staff: HasOne, } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] -pub enum RelatedEntity { - #[sea_orm(entity = "super::address::Entity")] - Address, - #[sea_orm(entity = "super::customer::Entity")] - Customer, - #[sea_orm(entity = "super::inventory::Entity")] - Inventory, - #[sea_orm(entity = "super::staff::Entity")] - Staff, -} From 73afda5f6cc2222ac24be11455e57d83968eb455 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 25 Oct 2025 01:31:33 +0100 Subject: [PATCH 51/85] Optimize cursor encode / decode to not use vec --- examples/sqlite/tests/plural_query_tests.rs | 5 +- src/query/entity_object_relation.rs | 5 +- src/query/entity_object_via_relation.rs | 14 +- src/query/loader/loader_impl.rs | 42 +----- src/query/pagination.rs | 40 ++---- src/utilities.rs | 141 ++++++++++++++++---- 6 files changed, 135 insertions(+), 112 deletions(-) diff --git a/examples/sqlite/tests/plural_query_tests.rs b/examples/sqlite/tests/plural_query_tests.rs index b74b4848..c7f11ab5 100644 --- a/examples/sqlite/tests/plural_query_tests.rs +++ b/examples/sqlite/tests/plural_query_tests.rs @@ -10,10 +10,7 @@ async fn schema() -> Schema { fn assert_eq(a: Response, b: &str) { let json = a.data.into_json().unwrap(); println!("{}", serde_json::to_string(&json).unwrap()); - assert_eq!( - json, - serde_json::from_str::(b).unwrap() - ) + assert_eq!(json, serde_json::from_str::(b).unwrap()) } #[tokio::test] diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index 66287091..9ff5d311 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -3,10 +3,7 @@ use async_graphql::{ dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}, }; use heck::{ToLowerCamelCase, ToSnakeCase}; -use sea_orm::{ - DatabaseConnection, EntityTrait, QueryFilter, - QueryTrait, RelationDef, -}; +use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, QueryTrait, RelationDef}; use crate::{ apply_memory_pagination, get_filter_conditions, guard_error, loader_impl, pluralize_unique, diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 80f566e0..71a4b0c1 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -3,17 +3,13 @@ use async_graphql::{ dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}, }; use heck::{ToLowerCamelCase, ToSnakeCase}; -use sea_orm::{ - DatabaseConnection, EntityTrait, - QueryFilter, QueryTrait, Related, RelationDef, -}; +use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, QueryTrait, Related, RelationDef}; use crate::{ - apply_memory_pagination, get_filter_conditions, guard_error, - loader_impl, pluralize_unique, BuilderContext, Connection, ConnectionObjectBuilder, - DatabaseContext, EntityObjectBuilder, FilterInputBuilder, GuardAction, HashableGroupKey, - KeyComplex, OneToManyLoader, OneToOneLoader, OperationType, OrderInputBuilder, - PaginationInputBuilder, UserContext, + apply_memory_pagination, get_filter_conditions, guard_error, loader_impl, pluralize_unique, + BuilderContext, Connection, ConnectionObjectBuilder, DatabaseContext, EntityObjectBuilder, + FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, + OperationType, OrderInputBuilder, PaginationInputBuilder, UserContext, }; /// This builder produces a GraphQL field for an SeaORM entity related trait diff --git a/src/query/loader/loader_impl.rs b/src/query/loader/loader_impl.rs index f7917d22..39296875 100644 --- a/src/query/loader/loader_impl.rs +++ b/src/query/loader/loader_impl.rs @@ -1,4 +1,5 @@ //! Mostly copied from SeaORM +use crate::IdenIter; use sea_orm::{ dynamic, sea_query::{ColumnRef, DynIden, Expr, ExprTrait, IntoColumnRef, TableRef, ValueTuple}, @@ -257,44 +258,3 @@ fn create_table_columns(table: &TableRef, cols: &Identity) -> Vec { .map(Expr::col) .collect() } - -struct IdenIter<'a> { - identity: &'a Identity, - index: usize, -} - -impl<'a> IdenIter<'a> { - fn new(identity: &'a Identity) -> Self { - Self { identity, index: 0 } - } -} - -impl<'a> Iterator for IdenIter<'a> { - type Item = &'a DynIden; - - fn next(&mut self) -> Option { - let result = match self.identity { - Identity::Unary(iden1) => { - if self.index == 0 { - Some(iden1) - } else { - None - } - } - Identity::Binary(iden1, iden2) => match self.index { - 0 => Some(iden1), - 1 => Some(iden2), - _ => None, - }, - Identity::Ternary(iden1, iden2, iden3) => match self.index { - 0 => Some(iden1), - 1 => Some(iden2), - 2 => Some(iden3), - _ => None, - }, - Identity::Many(vec) => vec.get(self.index), - }; - self.index += 1; - result - } -} diff --git a/src/query/pagination.rs b/src/query/pagination.rs index 060ff9d2..be986534 100644 --- a/src/query/pagination.rs +++ b/src/query/pagination.rs @@ -5,8 +5,8 @@ use sea_orm::{ }; use crate::{ - decode_cursor, encode_cursor, map_cursor_values, BuilderContext, Connection, Edge, PageInfo, - PageInput, PaginationInfo, PaginationInput, + decode_cursor, encode_cursor, BuilderContext, Connection, Edge, PageInfo, PageInput, + PaginationInfo, PaginationInput, }; /// used to parse pagination input object and apply it to statement @@ -64,9 +64,7 @@ where let mut stmt = apply_stmt_cursor_by(stmt)?; if let Some(cursor) = cursor_object.cursor { - let values = decode_cursor(&cursor)?; - - let cursor_values: sea_orm::sea_query::value::ValueTuple = map_cursor_values(values)?; + let cursor_values = decode_cursor(&cursor)?; stmt.after(cursor_values); } @@ -79,11 +77,7 @@ where let last_node = data.last(); if let Some(node) = last_node { - let values: Vec = T::PrimaryKey::iter() - .map(|variant| node.get(variant.into_column())) - .collect(); - - let values = map_cursor_values(values)?; + let values = node.get_primary_key_value(); let next_data = next_stmt.first(1).after(values).all(db).await?; @@ -99,11 +93,7 @@ where let first_node = data.first(); if let Some(node) = first_node { - let values: Vec = T::PrimaryKey::iter() - .map(|variant| node.get(variant.into_column())) - .collect(); - - let values = map_cursor_values(values)?; + let values = node.get_primary_key_value(); let previous_data = previous_stmt.first(1).before(values).all(db).await?; @@ -116,9 +106,7 @@ where let edges: Vec> = data .into_iter() .map(|node| { - let values: Vec = T::PrimaryKey::iter() - .map(|variant| node.get(variant.into_column())) - .collect(); + let values = node.get_primary_key_value(); let cursor: String = encode_cursor(values); @@ -151,9 +139,7 @@ where let edges: Vec> = data .into_iter() .map(|node| { - let values: Vec = T::PrimaryKey::iter() - .map(|variant| node.get(variant.into_column())) - .collect(); + let values = node.get_primary_key_value(); let cursor: String = encode_cursor(values); @@ -192,9 +178,7 @@ where let edges: Vec> = data .into_iter() .map(|node| { - let values: Vec = T::PrimaryKey::iter() - .map(|variant| node.get(variant.into_column())) - .collect(); + let values = node.get_primary_key_value(); let cursor: String = encode_cursor(values); @@ -239,9 +223,7 @@ where let edges: Vec> = data .into_iter() .map(|node| { - let values: Vec = T::PrimaryKey::iter() - .map(|variant| node.get(variant.into_column())) - .collect(); + let values = node.get_primary_key_value(); let cursor: String = encode_cursor(values); @@ -288,9 +270,7 @@ where let edges: Vec> = data .into_iter() .map(|node| { - let values: Vec = T::PrimaryKey::iter() - .map(|variant| node.get(variant.into_column())) - .collect(); + let values = node.get_primary_key_value(); let cursor: String = encode_cursor(values); diff --git a/src/utilities.rs b/src/utilities.rs index 23abdb17..c6d7fbc0 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -1,12 +1,11 @@ use async_graphql::dynamic::FieldValue; use itertools::Itertools; -use sea_orm::sea_query::value::ValueTuple; +use sea_orm::{sea_query::ValueTuple, DynIden, Identity}; use std::any::Any; /// used to encode the primary key values of a SeaORM entity to a String -pub fn encode_cursor(values: Vec) -> String { - values - .iter() +pub fn encode_cursor(values: ValueTuple) -> String { + ValueTupleIter::new(&values) .map(|value| -> String { match value { sea_orm::Value::TinyInt(value) => { @@ -106,31 +105,40 @@ pub enum DecodeMode { Data, } -pub fn map_cursor_values(values: Vec) -> Result { - if values.is_empty() { - Err(sea_orm::DbErr::Type("Missing cursor value".into())) - } else if values.len() == 1 { - Ok(ValueTuple::One(values[0].clone())) - } else if values.len() == 2 { - Ok(ValueTuple::Two(values[0].clone(), values[1].clone())) - } else if values.len() == 3 { - Ok(ValueTuple::Three( - values[0].clone(), - values[1].clone(), - values[2].clone(), - )) - } else { - Err(sea_orm::DbErr::Type( - "seaography does not support cursors values with size greater than 3".into(), - )) +#[derive(Default)] +struct ValueTupleBuilder(Option); + +impl ValueTupleBuilder { + fn push(&mut self, value: sea_orm::Value) { + match self.0.take() { + None => { + self.0 = Some(ValueTuple::One(value)); + } + Some(ValueTuple::One(a)) => { + self.0 = Some(ValueTuple::Two(a, value)); + } + Some(ValueTuple::Two(a, b)) => { + self.0 = Some(ValueTuple::Three(a, b, value)); + } + Some(ValueTuple::Three(a, b, c)) => { + self.0 = Some(ValueTuple::Many(vec![a, b, c, value])); + } + Some(ValueTuple::Many(mut items)) => { + items.push(value); + self.0 = Some(ValueTuple::Many(items)); + } + Some(_) => { + unimplemented!(); + } + } } } /// used to decode a String to a vector of SeaORM values -pub fn decode_cursor(s: &str) -> Result, sea_orm::DbErr> { +pub fn decode_cursor(s: &str) -> Result { let chars = s.chars(); - let mut values: Vec = vec![]; + let mut values = ValueTupleBuilder::default(); let mut type_indicator = String::new(); let mut length_indicator = String::new(); @@ -280,7 +288,9 @@ pub fn decode_cursor(s: &str) -> Result, sea_orm::DbErr> { } } - Ok(values) + values + .0 + .ok_or_else(|| sea_orm::DbErr::Type("Missing cursor value".into())) } #[cfg(feature = "field-pluralize")] @@ -321,3 +331,86 @@ pub fn try_downcast_ref<'a, T: Any>(value: &'a FieldValue<'a>) -> async_graphql: .into()), } } + +pub(crate) struct IdenIter<'a> { + identity: &'a Identity, + index: usize, +} + +impl<'a> IdenIter<'a> { + pub fn new(identity: &'a Identity) -> Self { + Self { identity, index: 0 } + } +} + +impl<'a> Iterator for IdenIter<'a> { + type Item = &'a DynIden; + + fn next(&mut self) -> Option { + let result = match self.identity { + Identity::Unary(iden1) => { + if self.index == 0 { + Some(iden1) + } else { + None + } + } + Identity::Binary(iden1, iden2) => match self.index { + 0 => Some(iden1), + 1 => Some(iden2), + _ => None, + }, + Identity::Ternary(iden1, iden2, iden3) => match self.index { + 0 => Some(iden1), + 1 => Some(iden2), + 2 => Some(iden3), + _ => None, + }, + Identity::Many(vec) => vec.get(self.index), + }; + self.index += 1; + result + } +} + +pub(crate) struct ValueTupleIter<'a> { + value: &'a ValueTuple, + index: usize, +} + +impl<'a> ValueTupleIter<'a> { + pub fn new(value: &'a ValueTuple) -> Self { + Self { value, index: 0 } + } +} + +impl<'a> Iterator for ValueTupleIter<'a> { + type Item = &'a sea_orm::Value; + + fn next(&mut self) -> Option { + let result = match self.value { + ValueTuple::One(a) => { + if self.index == 0 { + Some(a) + } else { + None + } + } + ValueTuple::Two(a, b) => match self.index { + 0 => Some(a), + 1 => Some(b), + _ => None, + }, + ValueTuple::Three(a, b, c) => match self.index { + 0 => Some(a), + 1 => Some(b), + 2 => Some(c), + _ => None, + }, + ValueTuple::Many(vec) => vec.get(self.index), + _ => unimplemented!(), + }; + self.index += 1; + result + } +} From 43933006a95b1e2070236fa1d13c7b18c01c3160 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 25 Oct 2025 02:03:01 +0100 Subject: [PATCH 52/85] Stop using smallint for FKs --- examples/postgres/README.md | 1 + examples/postgres/sakila-schema.sql | 44 +++++++++---------- examples/postgres/src/entities/actor.rs | 2 + examples/postgres/src/entities/address.rs | 4 +- examples/postgres/src/entities/category.rs | 2 + examples/postgres/src/entities/city.rs | 4 +- examples/postgres/src/entities/country.rs | 2 + examples/postgres/src/entities/customer.rs | 6 ++- examples/postgres/src/entities/film.rs | 12 +++-- examples/postgres/src/entities/film_actor.rs | 6 ++- .../postgres/src/entities/film_category.rs | 6 ++- examples/postgres/src/entities/inventory.rs | 6 ++- examples/postgres/src/entities/language.rs | 2 + examples/postgres/src/entities/mod.rs | 2 + examples/postgres/src/entities/payment.rs | 6 ++- examples/postgres/src/entities/prelude.rs | 2 + examples/postgres/src/entities/rental.rs | 6 ++- .../src/entities/sea_orm_active_enums.rs | 2 + examples/postgres/src/entities/staff.rs | 6 ++- examples/postgres/src/entities/store.rs | 6 ++- examples/postgres/tests/query_tests.rs | 1 + 21 files changed, 84 insertions(+), 44 deletions(-) diff --git a/examples/postgres/README.md b/examples/postgres/README.md index 24af6e4d..b4aee439 100644 --- a/examples/postgres/README.md +++ b/examples/postgres/README.md @@ -6,6 +6,7 @@ psql -q postgres://sea:sea@localhost/postgres -c 'CREATE DATABASE "sakila"' psql -q postgres://sea:sea@localhost/sakila < sakila-schema.sql psql -q postgres://sea:sea@localhost/sakila < sakila-data.sql +psql -q postgres://sea:sea@localhost/sakila < sakila-patch.sql ``` ```sh diff --git a/examples/postgres/sakila-schema.sql b/examples/postgres/sakila-schema.sql index 93355446..876209b9 100644 --- a/examples/postgres/sakila-schema.sql +++ b/examples/postgres/sakila-schema.sql @@ -159,11 +159,11 @@ CREATE TABLE film ( title character varying(255) NOT NULL, description text, release_year year, - language_id smallint NOT NULL, - original_language_id smallint, - rental_duration smallint DEFAULT 3 NOT NULL, + language_id integer NOT NULL, + original_language_id integer, + rental_duration integer DEFAULT 3 NOT NULL, rental_rate numeric(4,2) DEFAULT 4.99 NOT NULL, - length smallint, + length integer, replacement_cost numeric(5,2) DEFAULT 19.99 NOT NULL, rating mpaa_rating DEFAULT 'G'::mpaa_rating, last_update timestamp without time zone DEFAULT now() NOT NULL, @@ -179,8 +179,8 @@ ALTER TABLE public.film OWNER TO postgres; -- CREATE TABLE film_actor ( - actor_id smallint NOT NULL, - film_id smallint NOT NULL, + actor_id integer NOT NULL, + film_id integer NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL ); @@ -192,8 +192,8 @@ ALTER TABLE public.film_actor OWNER TO postgres; -- CREATE TABLE film_category ( - film_id smallint NOT NULL, - category_id smallint NOT NULL, + film_id integer NOT NULL, + category_id integer NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL ); @@ -232,7 +232,7 @@ CREATE TABLE address ( address character varying(50) NOT NULL, address2 character varying(50), district character varying(20) NOT NULL, - city_id smallint NOT NULL, + city_id integer NOT NULL, postal_code character varying(10), phone character varying(20) NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL @@ -261,7 +261,7 @@ ALTER TABLE public.city_city_id_seq OWNER TO postgres; CREATE TABLE city ( city_id integer DEFAULT nextval('city_city_id_seq'::regclass) NOT NULL, city character varying(50) NOT NULL, - country_id smallint NOT NULL, + country_id integer NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL ); @@ -313,11 +313,11 @@ ALTER TABLE public.customer_customer_id_seq OWNER TO postgres; CREATE TABLE customer ( customer_id integer DEFAULT nextval('customer_customer_id_seq'::regclass) NOT NULL, - store_id smallint NOT NULL, + store_id integer NOT NULL, first_name character varying(45) NOT NULL, last_name character varying(45) NOT NULL, email character varying(50), - address_id smallint NOT NULL, + address_id integer NOT NULL, activebool boolean DEFAULT true NOT NULL, create_date date DEFAULT ('now'::text)::date NOT NULL, last_update timestamp without time zone DEFAULT now(), @@ -366,8 +366,8 @@ ALTER TABLE public.inventory_inventory_id_seq OWNER TO postgres; CREATE TABLE inventory ( inventory_id integer DEFAULT nextval('inventory_inventory_id_seq'::regclass) NOT NULL, - film_id smallint NOT NULL, - store_id smallint NOT NULL, + film_id integer NOT NULL, + store_id integer NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL ); @@ -429,8 +429,8 @@ ALTER TABLE public.payment_payment_id_seq OWNER TO postgres; CREATE TABLE payment ( payment_id integer DEFAULT nextval('payment_payment_id_seq'::regclass) NOT NULL, - customer_id smallint NOT NULL, - staff_id smallint NOT NULL, + customer_id integer NOT NULL, + staff_id integer NOT NULL, rental_id integer NOT NULL, amount numeric(5,2) NOT NULL, payment_date timestamp without time zone NOT NULL @@ -460,9 +460,9 @@ CREATE TABLE rental ( rental_id integer DEFAULT nextval('rental_rental_id_seq'::regclass) NOT NULL, rental_date timestamp without time zone NOT NULL, inventory_id integer NOT NULL, - customer_id smallint NOT NULL, + customer_id integer NOT NULL, return_date timestamp without time zone, - staff_id smallint NOT NULL, + staff_id integer NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL ); @@ -500,10 +500,10 @@ CREATE TABLE staff ( staff_id integer DEFAULT nextval('staff_staff_id_seq'::regclass) NOT NULL, first_name character varying(45) NOT NULL, last_name character varying(45) NOT NULL, - address_id smallint NOT NULL, + address_id integer NOT NULL, reports_to_id integer, email character varying(50), - store_id smallint NOT NULL, + store_id integer NOT NULL, active boolean DEFAULT true NOT NULL, username character varying(16) NOT NULL, password character varying(40), @@ -533,8 +533,8 @@ ALTER TABLE public.store_store_id_seq OWNER TO postgres; CREATE TABLE store ( store_id integer DEFAULT nextval('store_store_id_seq'::regclass) NOT NULL, - manager_staff_id smallint NOT NULL, - address_id smallint NOT NULL, + manager_staff_id integer NOT NULL, + address_id integer NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL ); diff --git a/examples/postgres/src/entities/actor.rs b/examples/postgres/src/entities/actor.rs index 2ef07b93..39aa1530 100644 --- a/examples/postgres/src/entities/actor.rs +++ b/examples/postgres/src/entities/actor.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] diff --git a/examples/postgres/src/entities/address.rs b/examples/postgres/src/entities/address.rs index cd28df38..87f34442 100644 --- a/examples/postgres/src/entities/address.rs +++ b/examples/postgres/src/entities/address.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -8,7 +10,7 @@ pub struct Model { pub address: String, pub address2: Option, pub district: String, - pub city_id: i16, + pub city_id: i32, pub postal_code: Option, pub phone: String, pub last_update: DateTime, diff --git a/examples/postgres/src/entities/category.rs b/examples/postgres/src/entities/category.rs index ff8024f3..d1c80d3c 100644 --- a/examples/postgres/src/entities/category.rs +++ b/examples/postgres/src/entities/category.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] diff --git a/examples/postgres/src/entities/city.rs b/examples/postgres/src/entities/city.rs index e7a6f827..e79c17b0 100644 --- a/examples/postgres/src/entities/city.rs +++ b/examples/postgres/src/entities/city.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -6,7 +8,7 @@ pub struct Model { #[sea_orm(primary_key)] pub city_id: i32, pub city: String, - pub country_id: i16, + pub country_id: i32, pub last_update: DateTime, } diff --git a/examples/postgres/src/entities/country.rs b/examples/postgres/src/entities/country.rs index 277524ff..ea231ae9 100644 --- a/examples/postgres/src/entities/country.rs +++ b/examples/postgres/src/entities/country.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] diff --git a/examples/postgres/src/entities/customer.rs b/examples/postgres/src/entities/customer.rs index 41a90f96..4703624f 100644 --- a/examples/postgres/src/entities/customer.rs +++ b/examples/postgres/src/entities/customer.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -5,11 +7,11 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub customer_id: i32, - pub store_id: i16, + pub store_id: i32, pub first_name: String, pub last_name: String, pub email: Option, - pub address_id: i16, + pub address_id: i32, pub activebool: bool, pub create_date: Date, pub last_update: Option, diff --git a/examples/postgres/src/entities/film.rs b/examples/postgres/src/entities/film.rs index 8abc5062..89089769 100644 --- a/examples/postgres/src/entities/film.rs +++ b/examples/postgres/src/entities/film.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use super::sea_orm_active_enums::MpaaRating; use sea_orm::entity::prelude::*; @@ -10,17 +12,19 @@ pub struct Model { #[sea_orm(column_type = "Text", nullable)] pub description: Option, pub release_year: Option, - pub language_id: i16, - pub original_language_id: Option, - pub rental_duration: i16, + pub language_id: i32, + pub original_language_id: Option, + pub rental_duration: i32, #[sea_orm(column_type = "Decimal(Some((4, 2)))")] pub rental_rate: Decimal, - pub length: Option, + pub length: Option, #[sea_orm(column_type = "Decimal(Some((5, 2)))")] pub replacement_cost: Decimal, pub rating: Option, pub last_update: DateTime, pub special_features: Option>, + #[sea_orm(ignore, column_type = "custom(\"tsvector\")", select_as = "text")] + pub fulltext: String, #[sea_orm(column_type = "JsonBinary", nullable)] pub metadata: Option, } diff --git a/examples/postgres/src/entities/film_actor.rs b/examples/postgres/src/entities/film_actor.rs index 792f1ddc..8d9ee9ad 100644 --- a/examples/postgres/src/entities/film_actor.rs +++ b/examples/postgres/src/entities/film_actor.rs @@ -1,12 +1,14 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "film_actor")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub actor_id: i16, + pub actor_id: i32, #[sea_orm(primary_key, auto_increment = false)] - pub film_id: i16, + pub film_id: i32, pub last_update: DateTime, } diff --git a/examples/postgres/src/entities/film_category.rs b/examples/postgres/src/entities/film_category.rs index 78d97158..7a553dbc 100644 --- a/examples/postgres/src/entities/film_category.rs +++ b/examples/postgres/src/entities/film_category.rs @@ -1,12 +1,14 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "film_category")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub film_id: i16, + pub film_id: i32, #[sea_orm(primary_key, auto_increment = false)] - pub category_id: i16, + pub category_id: i32, pub last_update: DateTime, } diff --git a/examples/postgres/src/entities/inventory.rs b/examples/postgres/src/entities/inventory.rs index 921ed61c..2a6e7e75 100644 --- a/examples/postgres/src/entities/inventory.rs +++ b/examples/postgres/src/entities/inventory.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -5,8 +7,8 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub inventory_id: i32, - pub film_id: i16, - pub store_id: i16, + pub film_id: i32, + pub store_id: i32, pub last_update: DateTime, } diff --git a/examples/postgres/src/entities/language.rs b/examples/postgres/src/entities/language.rs index 4e3709b6..fb1954c4 100644 --- a/examples/postgres/src/entities/language.rs +++ b/examples/postgres/src/entities/language.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] diff --git a/examples/postgres/src/entities/mod.rs b/examples/postgres/src/entities/mod.rs index 2c373dd3..92015212 100644 --- a/examples/postgres/src/entities/mod.rs +++ b/examples/postgres/src/entities/mod.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + pub mod prelude; pub mod actor; diff --git a/examples/postgres/src/entities/payment.rs b/examples/postgres/src/entities/payment.rs index 82527368..26ab0bd9 100644 --- a/examples/postgres/src/entities/payment.rs +++ b/examples/postgres/src/entities/payment.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -5,8 +7,8 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub payment_id: i32, - pub customer_id: i16, - pub staff_id: i16, + pub customer_id: i32, + pub staff_id: i32, pub rental_id: i32, #[sea_orm(column_type = "Decimal(Some((5, 2)))")] pub amount: Decimal, diff --git a/examples/postgres/src/entities/prelude.rs b/examples/postgres/src/entities/prelude.rs index 27f39d53..52d81d85 100644 --- a/examples/postgres/src/entities/prelude.rs +++ b/examples/postgres/src/entities/prelude.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + pub use super::actor::Entity as Actor; pub use super::address::Entity as Address; pub use super::category::Entity as Category; diff --git a/examples/postgres/src/entities/rental.rs b/examples/postgres/src/entities/rental.rs index 7fccf410..798460a2 100644 --- a/examples/postgres/src/entities/rental.rs +++ b/examples/postgres/src/entities/rental.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -7,9 +9,9 @@ pub struct Model { pub rental_id: i32, pub rental_date: DateTime, pub inventory_id: i32, - pub customer_id: i16, + pub customer_id: i32, pub return_date: Option, - pub staff_id: i16, + pub staff_id: i32, pub last_update: DateTime, } diff --git a/examples/postgres/src/entities/sea_orm_active_enums.rs b/examples/postgres/src/entities/sea_orm_active_enums.rs index 94dde1a8..6cc9bdcf 100644 --- a/examples/postgres/src/entities/sea_orm_active_enums.rs +++ b/examples/postgres/src/entities/sea_orm_active_enums.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] diff --git a/examples/postgres/src/entities/staff.rs b/examples/postgres/src/entities/staff.rs index 87ef03db..5b2a8ffe 100644 --- a/examples/postgres/src/entities/staff.rs +++ b/examples/postgres/src/entities/staff.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -7,10 +9,10 @@ pub struct Model { pub staff_id: i32, pub first_name: String, pub last_name: String, - pub address_id: i16, + pub address_id: i32, pub reports_to_id: Option, pub email: Option, - pub store_id: i16, + pub store_id: i32, pub active: bool, pub username: String, pub password: Option, diff --git a/examples/postgres/src/entities/store.rs b/examples/postgres/src/entities/store.rs index 44552fe0..c4c7c70a 100644 --- a/examples/postgres/src/entities/store.rs +++ b/examples/postgres/src/entities/store.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -6,8 +8,8 @@ pub struct Model { #[sea_orm(primary_key)] pub store_id: i32, #[sea_orm(unique)] - pub manager_staff_id: i16, - pub address_id: i16, + pub manager_staff_id: i32, + pub address_id: i32, pub last_update: DateTime, } diff --git a/examples/postgres/tests/query_tests.rs b/examples/postgres/tests/query_tests.rs index c53d7254..a1b30a4d 100644 --- a/examples/postgres/tests/query_tests.rs +++ b/examples/postgres/tests/query_tests.rs @@ -841,6 +841,7 @@ async fn enumeration_filter() { film( filters: { rating: { eq: NC17 } } pagination: { page: { page: 1, limit: 5 } } + orderBy: { filmId: ASC } ) { nodes { filmId From 9706b0be4c0c80cb682cc350879897caba184d37 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 25 Oct 2025 16:19:34 +0100 Subject: [PATCH 53/85] Deprecate async-std --- cli/Cargo.toml | 4 ++-- cli/src/main.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3bb57d7f..c3701762 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0-rc.3" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] -description = "🧭 A GraphQL framework for SeaORM" +description = "🧭 A GraphQL framework for Rust" license = "MIT OR Apache-2.0" homepage = "https://www.sea-ql.org/Seaography" documentation = "https://docs.rs/seaography" @@ -13,7 +13,7 @@ keywords = ["async", "graphql", "mysql", "postgres", "sqlite"] categories = ["database"] [dependencies] -async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } clap = { version = "4.3.19", features = ["env", "derive"] } seaography-generator = { version = "~2.0.0-rc.3", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index e0d804c4..8b14e460 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -94,7 +94,7 @@ pub fn parse_database_url(database_url: &str) -> Result Date: Sun, 26 Oct 2025 10:58:58 +0000 Subject: [PATCH 54/85] clippy --- src/query/loader/impl_traits.rs | 1 - src/utilities.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/query/loader/impl_traits.rs b/src/query/loader/impl_traits.rs index 7f8867f7..c586a877 100644 --- a/src/query/loader/impl_traits.rs +++ b/src/query/loader/impl_traits.rs @@ -146,7 +146,6 @@ impl<'a> Iterator for ValueTupleIter<'a> { _ => None, }, ValueTuple::Many(vec) => vec.get(self.index), - _ => unreachable!(), }; self.index += 1; result diff --git a/src/utilities.rs b/src/utilities.rs index c6d7fbc0..08680704 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -127,9 +127,6 @@ impl ValueTupleBuilder { items.push(value); self.0 = Some(ValueTuple::Many(items)); } - Some(_) => { - unimplemented!(); - } } } } @@ -408,7 +405,6 @@ impl<'a> Iterator for ValueTupleIter<'a> { _ => None, }, ValueTuple::Many(vec) => vec.get(self.index), - _ => unimplemented!(), }; self.index += 1; result From c8e006ed395e357e4c296b65afb901c8ed46d999 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 26 Oct 2025 11:02:39 +0000 Subject: [PATCH 55/85] Fix has_one relation --- src/query/entity_object_relation.rs | 11 ++++++----- src/query/entity_object_via_relation.rs | 17 +++++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index 9ff5d311..bb6780f3 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -63,8 +63,8 @@ impl EntityObjectRelationBuilder { let relation_definition_is_owner = relation_definition.is_owner; let field_name = name.clone(); - let field = match relation_definition.is_owner { - false => Field::new(name, TypeRef::named(&object_name), move |ctx| { + let field = if !relation_definition.is_owner { + Field::new(name, TypeRef::named(&object_name), move |ctx| { let object_name = object_name.clone(); let parent_name = parent_name.clone(); let field_name = field_name.clone(); @@ -131,8 +131,9 @@ impl EntityObjectRelationBuilder { Ok(None) } }) - }), - true => Field::new( + }) + } else { + Field::new( name, TypeRef::named_nn(connection_object_builder.type_name(&object_name)), move |ctx| { @@ -207,7 +208,7 @@ impl EntityObjectRelationBuilder { Ok(Some(FieldValue::owned_any(connection))) }) }, - ), + ) }; match relation_definition_is_owner { diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 71a4b0c1..61df0beb 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -3,7 +3,9 @@ use async_graphql::{ dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}, }; use heck::{ToLowerCamelCase, ToSnakeCase}; -use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, QueryTrait, Related, RelationDef}; +use sea_orm::{ + DatabaseConnection, EntityTrait, QueryFilter, QueryTrait, Related, RelationDef, RelationType, +}; use crate::{ apply_memory_pagination, get_filter_conditions, guard_error, loader_impl, pluralize_unique, @@ -70,8 +72,10 @@ impl EntityObjectViaRelationBuilder { let hooks = &self.context.hooks; let field_name = name.clone(); - let field = match via_rel_def.is_owner { - false => Field::new(name, TypeRef::named(&object_name), move |ctx| { + let field = if !via_rel_def.is_owner + || (!is_via_relation && to_rel_def.rel_type == RelationType::HasOne) + { + Field::new(name, TypeRef::named(&object_name), move |ctx| { let object_name = object_name.clone(); let parent_name = parent_name.clone(); let field_name = field_name.clone(); @@ -135,8 +139,9 @@ impl EntityObjectViaRelationBuilder { Ok(None) } }) - }), - true => Field::new( + }) + } else { + Field::new( name, TypeRef::named_nn(connection_object_builder.type_name(&object_name)), move |ctx| { @@ -234,7 +239,7 @@ impl EntityObjectViaRelationBuilder { Ok(Some(FieldValue::owned_any(connection))) }) }, - ), + ) }; match via_rel_def_is_owner { From b9d17266d4385e99ad9be457ae85613f2cb8d35f Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 26 Oct 2025 11:53:39 +0000 Subject: [PATCH 56/85] Add test case for diamond relations --- examples/sqlite/tests/mutation_tests.rs | 74 +++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/examples/sqlite/tests/mutation_tests.rs b/examples/sqlite/tests/mutation_tests.rs index 1cc9e370..45573a9e 100644 --- a/examples/sqlite/tests/mutation_tests.rs +++ b/examples/sqlite/tests/mutation_tests.rs @@ -9,6 +9,7 @@ async fn main() { test_create_batch_mutation().await; test_update_mutation().await; test_delete_mutation().await; + test_add_original_language_to_film().await; } async fn schema() -> Schema { @@ -601,3 +602,76 @@ async fn test_delete_mutation() { "#, ); } + +async fn test_add_original_language_to_film() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + mutation { + filmUpdate( + data: { originalLanguageId: 5 } + filter: { filmId: { eq: 500 } } + ) { + filmId + title + } + } + "#, + ) + .await, + r#" + { + "filmUpdate": [ + { + "filmId": 500, + "title": "KISS GLORY" + } + ] + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + film(filters: { filmId: { eq: 500 } }) { + nodes { + filmId + title + language1 { + name + } + language2 { + name + } + } + } + } + "#, + ) + .await, + r#" + { + "film": { + "nodes": [ + { + "filmId": 500, + "title": "KISS GLORY", + "language1": { + "name": "English" + }, + "language2": { + "name": "French" + } + } + ] + } + } + "#, + ); +} From d1c765ae0a31746c2405494a628e7370c6122743 Mon Sep 17 00:00:00 2001 From: Peter Kelly Date: Mon, 27 Oct 2025 17:19:38 +0700 Subject: [PATCH 57/85] Add 'not' operator to filters (#231) Seaography already supports 'and' and 'or' operators on filters. This commit adds a 'not' operator, which takes a filter object as an argument and maps this onto the corresponding Condition not() method in SeaORM. This is particularly useful for using operators that do not have an inverse version, such as array contains. For example, in one use case, we want to select all entities whose tags field does _not_ contain a specific value: { runs(filters: { not: { tags: { array_contains: ["disabled"] } } }) { nodes { id name status } } } --- examples/sea-draw/src/client_test.rs | 107 +++++++++++++++++++++++++++ src/inputs/filter_input.rs | 1 + src/query/filtering.rs | 7 ++ 3 files changed, 115 insertions(+) diff --git a/examples/sea-draw/src/client_test.rs b/examples/sea-draw/src/client_test.rs index ebe25cf4..62036f84 100644 --- a/examples/sea-draw/src/client_test.rs +++ b/examples/sea-draw/src/client_test.rs @@ -25,6 +25,7 @@ pub async fn client_test( test_permissions(&url, root_account_id).await; test_entities(&url, root_account_id).await; + test_boolean_operators(&url, root_account_id).await; tracing::info!("All tests completed successfully!"); @@ -329,6 +330,112 @@ async fn test_permissions(url: &str, root_account_id: Uuid) { ); } +#[instrument(skip_all)] +async fn test_boolean_operators(url: &str, root_account_id: Uuid) { + let account_id = create_account(url, root_account_id, "Account 1").await; + let client = Client::new(url, account_id); + let project_id = client.create_project("Project 1").await.unwrap(); + + for (name, width, height) in [ + ("Drawing 0", 300, 100), + ("Drawing 1", 400, 110), + ("Drawing 2", 500, 120), + ("Drawing 3", 600, 130), + ("Drawing 4", 700, 140), + ("Drawing 5", 300, 150), + ("Drawing 6", 400, 160), + ("Drawing 7", 500, 170), + ("Drawing 8", 600, 180), + ("Drawing 9", 700, 190), + ] { + client + .create_drawing(project_id, name, width, height) + .await + .unwrap(); + } + + // not + let query = r#"{ drawings(order_by: { name: ASC }, filters: { + not: { width: { gt: 400 } } + }) { nodes { name width height } } }"#; + let result = graphql(url, account_id, query, None).await.unwrap(); + let expected1 = json!({ + "drawings": { + "nodes": [ + { "name": "Drawing 0", "width": 300, "height": 100}, + { "name": "Drawing 1", "width": 400, "height": 110}, + { "name": "Drawing 5", "width": 300, "height": 150}, + { "name": "Drawing 6", "width": 400, "height": 160} + ] + } + }); + assert_eq!(result, expected1); + + // and + let expected2 = json!({ + "drawings": { + "nodes": [ + { "name": "Drawing 2", "width": 500, "height": 120 }, + { "name": "Drawing 3", "width": 600, "height": 130 }, + { "name": "Drawing 4", "width": 700, "height": 140 }, + { "name": "Drawing 7", "width": 500, "height": 170 } + ] + } + }); + + let query = r#"{ drawings(order_by: { name: ASC }, filters: { + and: [ + { width: { gte: 500 } } + { height: { lte: 170 } } + ] + }) { nodes { name width height } } }"#; + let result = graphql(url, account_id, query, None).await.unwrap(); + assert_eq!(result, expected2); + + // and + not + // Same as above but with conditions inverted + let query = r#"{ drawings(order_by: { name: ASC }, filters: { + and: [ + { not: { width: { lt: 500 } } } + { not: { height: { gt: 170 } } } + ] + }) { nodes { name width height } } }"#; + let result = graphql(url, account_id, query, None).await.unwrap(); + assert_eq!(result, expected2); + + let expected3 = json!({ + "drawings": { + "nodes": [ + { "name": "Drawing 0", "width": 300, "height": 100 }, + { "name": "Drawing 1", "width": 400, "height": 110 }, + { "name": "Drawing 4", "width": 700, "height": 140 }, + { "name": "Drawing 9", "width": 700, "height": 190 } + ] + } + }); + + // or + let query = r#"{ drawings(order_by: { name: ASC }, filters: { + or: [ + { width: { gte: 700 } } + { height: { lte: 110 } } + ] + }) { nodes { name width height } } }"#; + let result = graphql(url, account_id, query, None).await.unwrap(); + assert_eq!(result, expected3); + + // or + not + // Same as above but with conditions inverted + let query = r#"{ drawings(order_by: { name: ASC }, filters: { + or: [ + { not: { width: { lt: 700 } } } + { not: { height: { gt: 110 } } } + ] + }) { nodes { name width height } } }"#; + let result = graphql(url, account_id, query, None).await.unwrap(); + assert_eq!(result, expected3); +} + pub async fn create_account(url: &str, root_account_id: Uuid, name: &str) -> Uuid { let result = graphql( url, diff --git a/src/inputs/filter_input.rs b/src/inputs/filter_input.rs index 0aa561a8..3c02c3ec 100644 --- a/src/inputs/filter_input.rs +++ b/src/inputs/filter_input.rs @@ -60,5 +60,6 @@ impl FilterInputBuilder { object .field(InputValue::new("and", TypeRef::named_nn_list(&filter_name))) .field(InputValue::new("or", TypeRef::named_nn_list(&filter_name))) + .field(InputValue::new("not", TypeRef::named(&filter_name))) } } diff --git a/src/query/filtering.rs b/src/query/filtering.rs index 1393b7b8..de625633 100644 --- a/src/query/filtering.rs +++ b/src/query/filtering.rs @@ -81,5 +81,12 @@ where condition }; + let condition = if let Some(not) = filters.get("not") { + let nested_condition = recursive_prepare_condition::(context, ¬.object()?)?; + condition.add(nested_condition.not()) + } else { + condition + }; + Ok(condition) } From fe4dfe6551bdbea5957b445a4cc9fb9718b2466d Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 28 Oct 2025 00:42:17 +0000 Subject: [PATCH 58/85] 2.0.0-rc.4 --- Cargo.toml | 4 ++-- cli/Cargo.toml | 4 ++-- generator/Cargo.toml | 2 +- macros/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b70876a..b1b9245e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" edition = "2021" rust-version = "1.70" authors = [ @@ -25,7 +25,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } -seaography-macros = { version = "~2.0.0-rc.3", path = "macros", optional = true } +seaography-macros = { version = "~2.0.0-rc.4", path = "macros", optional = true } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c3701762..ac6da0b7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-cli" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } clap = { version = "4.3.19", features = ["env", "derive"] } -seaography-generator = { version = "~2.0.0-rc.3", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc.4", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/generator/Cargo.toml b/generator/Cargo.toml index 88d4069c..c09d3848 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-generator" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 6c54982d..b7352b49 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-macros" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" edition = "2024" rust-version = "1.85" authors = ["Chris Tsang "] From a1f1737077b6f35e6cc8d898d06ebfcd140c37de Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 28 Oct 2025 00:42:17 +0000 Subject: [PATCH 59/85] update examples --- examples/mysql/Cargo.toml | 4 ++-- examples/postgres/Cargo.toml | 4 ++-- examples/sea-draw/Cargo.toml | 4 ++-- examples/sqlite/Cargo.toml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index acb2fbd8..6c297c9d 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-mysql-example" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" [dependencies] actix-web = { version = "4.5", default-features = false, features = ["macros"] } @@ -17,7 +17,7 @@ features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.3" # seaography version +version = "~2.0.0-rc.4" # seaography version features = ["with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 955d90f7..7991da56 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" [dependencies] axum = { version = "0.8" } @@ -17,7 +17,7 @@ features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.3" # seaography version +version = "~2.0.0-rc.4" # seaography version features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index 8ae58a9b..576b5f44 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-draw" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" edition = "2024" [dependencies] @@ -30,7 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.3" # seaography version +version = "~2.0.0-rc.4" # seaography version features = [ "macros", "field-pluralize", diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 1e219766..9c7a7348 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "2.0.0-rc.3" +version = "2.0.0-rc.4" [dependencies] poem = { version = "3.0" } @@ -16,7 +16,7 @@ version = "~2.0.0-rc" features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] -version = "~2.0.0-rc.3" # seaography version +version = "~2.0.0-rc.4" # seaography version features = ["with-decimal", "with-chrono"] [dev-dependencies] From 85e9562a398cff35132e9ab165dbfd9afbab7c82 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 1 Nov 2025 20:51:46 +0000 Subject: [PATCH 60/85] Update Entity with unique key --- examples/mysql/src/entities/rental.rs | 3 +++ examples/postgres/src/entities/rental.rs | 3 +++ examples/sqlite/src/entities/rental.rs | 3 +++ 3 files changed, 9 insertions(+) diff --git a/examples/mysql/src/entities/rental.rs b/examples/mysql/src/entities/rental.rs index 977b40ab..76f83360 100644 --- a/examples/mysql/src/entities/rental.rs +++ b/examples/mysql/src/entities/rental.rs @@ -5,8 +5,11 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub rental_id: i32, + #[sea_orm(unique_key = "unique")] pub rental_date: DateTime, + #[sea_orm(unique_key = "unique")] pub inventory_id: i32, + #[sea_orm(unique_key = "unique")] pub customer_id: i32, pub return_date: Option, pub staff_id: i32, diff --git a/examples/postgres/src/entities/rental.rs b/examples/postgres/src/entities/rental.rs index 798460a2..e09d25a8 100644 --- a/examples/postgres/src/entities/rental.rs +++ b/examples/postgres/src/entities/rental.rs @@ -7,8 +7,11 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub rental_id: i32, + #[sea_orm(unique_key = "unique")] pub rental_date: DateTime, + #[sea_orm(unique_key = "unique")] pub inventory_id: i32, + #[sea_orm(unique_key = "unique")] pub customer_id: i32, pub return_date: Option, pub staff_id: i32, diff --git a/examples/sqlite/src/entities/rental.rs b/examples/sqlite/src/entities/rental.rs index 7d64ea32..67aefb72 100644 --- a/examples/sqlite/src/entities/rental.rs +++ b/examples/sqlite/src/entities/rental.rs @@ -8,8 +8,11 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub rental_id: i32, + #[sea_orm(unique_key = "unique")] pub rental_date: DateTimeUtc, + #[sea_orm(unique_key = "unique")] pub inventory_id: i32, + #[sea_orm(unique_key = "unique")] pub customer_id: i32, pub return_date: Option, pub staff_id: i16, From ea83834b43ba977bb977c528829328d4f5fd7ab5 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 4 Nov 2025 17:01:11 +0000 Subject: [PATCH 61/85] Readme --- README.md | 4 ++-- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 46bd62e5..fc32bc59 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Seaography logo

🧭 A GraphQL framework for Rust

-

Quick to start, type‑safe, powerful and extensible

+

The quickest way to launch a GraphQL backend

[![crate](https://img.shields.io/crates/v/seaography.svg)](https://crates.io/crates/seaography) [![docs](https://docs.rs/seaography/badge.svg)](https://docs.rs/seaography) @@ -15,7 +15,7 @@ ## Introduction -Seaography is a **GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), +Seaography is a **powerful and extensible GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), turning your database schema into a fully-typed GraphQL API with minimal effort. By leveraging async-graphql's dynamic schema engine, Seaography avoids the heavy code generation of static approaches, resulting in faster compile times. The generated schema stays in sync with your SeaORM entities, while still giving you full control to extend and customize it. diff --git a/src/lib.rs b/src/lib.rs index 289f10a0..632320e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ //! Seaography logo //! //!

🧭 A GraphQL framework for Rust

-//!

Quick to start, type‑safe, powerful and extensible

+//!

The quickest way to launch a GraphQL backend

//! //! [![crate](https://img.shields.io/crates/v/seaography.svg)](https://crates.io/crates/seaography) //! [![docs](https://docs.rs/seaography/badge.svg)](https://docs.rs/seaography) @@ -15,7 +15,7 @@ //! //! ## Introduction //! -//! Seaography is a **GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), +//! Seaography is a **powerful and extensible GraphQL framework for Rust** that bridges [SeaORM](https://www.sea-ql.org/SeaORM/) and [async-graphql](https://github.com/async-graphql/async-graphql), //! turning your database schema into a fully-typed GraphQL API with minimal effort. //! By leveraging async-graphql's dynamic schema engine, Seaography avoids the heavy code generation of static approaches, resulting in faster compile times. //! The generated schema stays in sync with your SeaORM entities, while still giving you full control to extend and customize it. From feee59dc8251b621cfc003ebfa7abbf86729378b Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Fri, 7 Nov 2025 18:49:30 +0000 Subject: [PATCH 62/85] Bump sea-orm-cli --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index fb99c61f..7166f3eb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -57,7 +57,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 2.0.0-rc.13 + version: 2.0.0-rc.17 - name: Additional Integration tests working-directory: ./examples/sqlite run: | From 2abb26ae4a803146d61198c338461626bf2f5ef9 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Fri, 7 Nov 2025 20:20:23 +0000 Subject: [PATCH 63/85] Regenerate sqlite entities as i64 --- examples/sqlite/src/entities/actor.rs | 6 +++--- examples/sqlite/src/entities/address.rs | 8 ++++---- examples/sqlite/src/entities/category.rs | 4 ++-- examples/sqlite/src/entities/city.rs | 6 +++--- examples/sqlite/src/entities/country.rs | 4 ++-- examples/sqlite/src/entities/customer.rs | 10 +++++----- examples/sqlite/src/entities/film.rs | 6 +++--- examples/sqlite/src/entities/film_actor.rs | 8 ++++---- examples/sqlite/src/entities/film_category.rs | 6 +++--- examples/sqlite/src/entities/film_text.rs | 4 ++-- examples/sqlite/src/entities/inventory.rs | 10 +++++----- examples/sqlite/src/entities/language.rs | 4 ++-- examples/sqlite/src/entities/mod.rs | 2 +- examples/sqlite/src/entities/payment.rs | 10 +++++----- examples/sqlite/src/entities/prelude.rs | 2 +- examples/sqlite/src/entities/rental.rs | 10 +++++----- examples/sqlite/src/entities/staff.rs | 8 ++++---- examples/sqlite/src/entities/store.rs | 8 ++++---- examples/sqlite/tests/hook_guard_tests.rs | 2 +- examples/sqlite/tests/plural_query_tests.rs | 20 +++++++++---------- examples/sqlite/tests/query_tests.rs | 20 +++++++++---------- 21 files changed, 79 insertions(+), 79 deletions(-) diff --git a/examples/sqlite/src/entities/actor.rs b/examples/sqlite/src/entities/actor.rs index 6a90adec..26cbb007 100644 --- a/examples/sqlite/src/entities/actor.rs +++ b/examples/sqlite/src/entities/actor.rs @@ -1,13 +1,13 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "actor")] pub struct Model { #[sea_orm(primary_key)] - pub actor_id: i32, + pub actor_id: i64, pub first_name: String, pub last_name: String, pub last_update: DateTimeUtc, diff --git a/examples/sqlite/src/entities/address.rs b/examples/sqlite/src/entities/address.rs index 957e074a..3c758c8c 100644 --- a/examples/sqlite/src/entities/address.rs +++ b/examples/sqlite/src/entities/address.rs @@ -1,17 +1,17 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "address")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub address_id: i32, + pub address_id: i64, pub address: String, pub address2: Option, pub district: String, - pub city_id: i32, + pub city_id: i64, pub postal_code: Option, pub phone: String, pub last_update: DateTimeUtc, diff --git a/examples/sqlite/src/entities/category.rs b/examples/sqlite/src/entities/category.rs index 85b37509..022a407c 100644 --- a/examples/sqlite/src/entities/category.rs +++ b/examples/sqlite/src/entities/category.rs @@ -1,9 +1,9 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "category")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/examples/sqlite/src/entities/city.rs b/examples/sqlite/src/entities/city.rs index 03524d62..bbdd2f8a 100644 --- a/examples/sqlite/src/entities/city.rs +++ b/examples/sqlite/src/entities/city.rs @@ -1,13 +1,13 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "city")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub city_id: i32, + pub city_id: i64, pub city: String, pub country_id: i16, pub last_update: DateTimeUtc, diff --git a/examples/sqlite/src/entities/country.rs b/examples/sqlite/src/entities/country.rs index 16a68349..78061040 100644 --- a/examples/sqlite/src/entities/country.rs +++ b/examples/sqlite/src/entities/country.rs @@ -1,9 +1,9 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "country")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/examples/sqlite/src/entities/customer.rs b/examples/sqlite/src/entities/customer.rs index 61ca4f45..6c6f74f0 100644 --- a/examples/sqlite/src/entities/customer.rs +++ b/examples/sqlite/src/entities/customer.rs @@ -1,18 +1,18 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "customer")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub customer_id: i32, - pub store_id: i32, + pub customer_id: i64, + pub store_id: i64, pub first_name: String, pub last_name: String, pub email: Option, - pub address_id: i32, + pub address_id: i64, pub active: i16, pub create_date: DateTimeUtc, pub last_update: DateTimeUtc, diff --git a/examples/sqlite/src/entities/film.rs b/examples/sqlite/src/entities/film.rs index 224146f0..c1d41a35 100644 --- a/examples/sqlite/src/entities/film.rs +++ b/examples/sqlite/src/entities/film.rs @@ -1,13 +1,13 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "film")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub film_id: i32, + pub film_id: i64, pub title: String, #[sea_orm(column_type = "Text", nullable)] pub description: Option, diff --git a/examples/sqlite/src/entities/film_actor.rs b/examples/sqlite/src/entities/film_actor.rs index dcdc6f08..ba665aa3 100644 --- a/examples/sqlite/src/entities/film_actor.rs +++ b/examples/sqlite/src/entities/film_actor.rs @@ -1,15 +1,15 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "film_actor")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub actor_id: i32, + pub actor_id: i64, #[sea_orm(primary_key, auto_increment = false)] - pub film_id: i32, + pub film_id: i64, pub last_update: DateTimeUtc, #[sea_orm( belongs_to, diff --git a/examples/sqlite/src/entities/film_category.rs b/examples/sqlite/src/entities/film_category.rs index 50c16a3b..72c77353 100644 --- a/examples/sqlite/src/entities/film_category.rs +++ b/examples/sqlite/src/entities/film_category.rs @@ -1,13 +1,13 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "film_category")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub film_id: i32, + pub film_id: i64, #[sea_orm(primary_key, auto_increment = false)] pub category_id: i16, pub last_update: DateTimeUtc, diff --git a/examples/sqlite/src/entities/film_text.rs b/examples/sqlite/src/entities/film_text.rs index bf7525c4..584b167c 100644 --- a/examples/sqlite/src/entities/film_text.rs +++ b/examples/sqlite/src/entities/film_text.rs @@ -1,9 +1,9 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "film_text")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/examples/sqlite/src/entities/inventory.rs b/examples/sqlite/src/entities/inventory.rs index 5b9fa16d..049e5a4c 100644 --- a/examples/sqlite/src/entities/inventory.rs +++ b/examples/sqlite/src/entities/inventory.rs @@ -1,15 +1,15 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "inventory")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub inventory_id: i32, - pub film_id: i32, - pub store_id: i32, + pub inventory_id: i64, + pub film_id: i64, + pub store_id: i64, pub last_update: DateTimeUtc, #[sea_orm( belongs_to, diff --git a/examples/sqlite/src/entities/language.rs b/examples/sqlite/src/entities/language.rs index 672ae22d..6cb20ea1 100644 --- a/examples/sqlite/src/entities/language.rs +++ b/examples/sqlite/src/entities/language.rs @@ -1,9 +1,9 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "language")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/examples/sqlite/src/entities/mod.rs b/examples/sqlite/src/entities/mod.rs index 0203059c..ffc09e46 100644 --- a/examples/sqlite/src/entities/mod.rs +++ b/examples/sqlite/src/entities/mod.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 pub mod prelude; diff --git a/examples/sqlite/src/entities/payment.rs b/examples/sqlite/src/entities/payment.rs index ff66c63c..24451ab2 100644 --- a/examples/sqlite/src/entities/payment.rs +++ b/examples/sqlite/src/entities/payment.rs @@ -1,16 +1,16 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "payment")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub payment_id: i32, - pub customer_id: i32, + pub payment_id: i64, + pub customer_id: i64, pub staff_id: i16, - pub rental_id: Option, + pub rental_id: Option, #[sea_orm(column_type = "Decimal(Some((5, 2)))")] pub amount: Decimal, pub payment_date: DateTimeUtc, diff --git a/examples/sqlite/src/entities/prelude.rs b/examples/sqlite/src/entities/prelude.rs index 74e18682..883d6258 100644 --- a/examples/sqlite/src/entities/prelude.rs +++ b/examples/sqlite/src/entities/prelude.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 pub use super::actor::Entity as Actor; pub use super::address::Entity as Address; diff --git a/examples/sqlite/src/entities/rental.rs b/examples/sqlite/src/entities/rental.rs index 67aefb72..be95e3f0 100644 --- a/examples/sqlite/src/entities/rental.rs +++ b/examples/sqlite/src/entities/rental.rs @@ -1,19 +1,19 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "rental")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub rental_id: i32, + pub rental_id: i64, #[sea_orm(unique_key = "unique")] pub rental_date: DateTimeUtc, #[sea_orm(unique_key = "unique")] - pub inventory_id: i32, + pub inventory_id: i64, #[sea_orm(unique_key = "unique")] - pub customer_id: i32, + pub customer_id: i64, pub return_date: Option, pub staff_id: i16, pub last_update: DateTimeUtc, diff --git a/examples/sqlite/src/entities/staff.rs b/examples/sqlite/src/entities/staff.rs index 02437093..44aef15a 100644 --- a/examples/sqlite/src/entities/staff.rs +++ b/examples/sqlite/src/entities/staff.rs @@ -1,21 +1,21 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "staff")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub staff_id: i16, pub first_name: String, pub last_name: String, - pub address_id: i32, + pub address_id: i64, pub reports_to_id: Option, #[sea_orm(column_type = "Blob", nullable)] pub picture: Option>, pub email: Option, - pub store_id: i32, + pub store_id: i64, pub active: i16, pub username: String, pub password: Option, diff --git a/examples/sqlite/src/entities/store.rs b/examples/sqlite/src/entities/store.rs index 70e75017..e51d2f19 100644 --- a/examples/sqlite/src/entities/store.rs +++ b/examples/sqlite/src/entities/store.rs @@ -1,15 +1,15 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; #[sea_orm::model] -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "store")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub store_id: i32, + pub store_id: i64, pub manager_staff_id: i16, - pub address_id: i32, + pub address_id: i64, pub last_update: DateTimeUtc, #[sea_orm( belongs_to, diff --git a/examples/sqlite/tests/hook_guard_tests.rs b/examples/sqlite/tests/hook_guard_tests.rs index 1cc3efb1..9a99e249 100644 --- a/examples/sqlite/tests/hook_guard_tests.rs +++ b/examples/sqlite/tests/hook_guard_tests.rs @@ -71,7 +71,7 @@ struct Log { #[derive(Default, Clone)] struct Permissions { - actors: BTreeSet, + actors: BTreeSet, staff: BTreeSet, } diff --git a/examples/sqlite/tests/plural_query_tests.rs b/examples/sqlite/tests/plural_query_tests.rs index c7f11ab5..f1742d6b 100644 --- a/examples/sqlite/tests/plural_query_tests.rs +++ b/examples/sqlite/tests/plural_query_tests.rs @@ -284,8 +284,8 @@ async fn test_cursor_pagination() { "pageInfo": { "hasPreviousPage": false, "hasNextPage": true, - "startCursor": "Int[3]:342", - "endCursor": "Int[4]:5550" + "startCursor": "BigInt[3]:342", + "endCursor": "BigInt[4]:5550" } } } @@ -361,8 +361,8 @@ async fn test_cursor_pagination_prev() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "startCursor": "Int[4]:6409", - "endCursor": "Int[4]:9803" + "startCursor": "BigInt[4]:6409", + "endCursor": "BigInt[4]:9803" } } } @@ -429,8 +429,8 @@ async fn test_cursor_pagination_no_next() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": false, - "startCursor": "Int[5]:15821", - "endCursor": "Int[5]:15850" + "startCursor": "BigInt[5]:15821", + "endCursor": "BigInt[5]:15850" } } } @@ -522,7 +522,7 @@ async fn related_queries_filters() { { customers( filters: { active: { eq: 0 } } - pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } + pagination: { cursor: { limit: 3, cursor: "BigInt[3]:271" } } ) { nodes { customerId @@ -612,7 +612,7 @@ async fn related_queries_filters() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "endCursor": "Int[3]:406" + "endCursor": "BigInt[3]:406" } } } @@ -724,7 +724,7 @@ async fn related_queries_pagination() { { customers( filters: { active: { eq: 0 } } - pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } + pagination: { cursor: { limit: 3, cursor: "BigInt[3]:271" } } ) { nodes { customerId @@ -842,7 +842,7 @@ async fn related_queries_pagination() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "endCursor": "Int[3]:406" + "endCursor": "BigInt[3]:406" } } } diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index cf3b872a..6bef62cd 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -312,8 +312,8 @@ async fn test_cursor_pagination() { "pageInfo": { "hasPreviousPage": false, "hasNextPage": true, - "startCursor": "Int[3]:342", - "endCursor": "Int[4]:5550" + "startCursor": "BigInt[3]:342", + "endCursor": "BigInt[4]:5550" } } } @@ -461,8 +461,8 @@ async fn test_cursor_pagination_prev() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "startCursor": "Int[4]:6409", - "endCursor": "Int[4]:9803" + "startCursor": "BigInt[4]:6409", + "endCursor": "BigInt[4]:9803" } } } @@ -529,8 +529,8 @@ async fn test_cursor_pagination_no_next() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": false, - "startCursor": "Int[5]:15821", - "endCursor": "Int[5]:15850" + "startCursor": "BigInt[5]:15821", + "endCursor": "BigInt[5]:15850" } } } @@ -622,7 +622,7 @@ async fn related_queries_filters() { { customer( filters: { active: { eq: 0 } } - pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } + pagination: { cursor: { limit: 3, cursor: "BigInt[3]:271" } } ) { nodes { customerId @@ -712,7 +712,7 @@ async fn related_queries_filters() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "endCursor": "Int[3]:406" + "endCursor": "BigInt[3]:406" } } } @@ -824,7 +824,7 @@ async fn related_queries_pagination() { { customer( filters: { active: { eq: 0 } } - pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } + pagination: { cursor: { limit: 3, cursor: "BigInt[3]:271" } } ) { nodes { customerId @@ -942,7 +942,7 @@ async fn related_queries_pagination() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "endCursor": "Int[3]:406" + "endCursor": "BigInt[3]:406" } } } From fc41b20e1c6a7a8bd9ccb3e3d3e005273da3656f Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 13 Nov 2025 10:34:39 +0000 Subject: [PATCH 64/85] Bump --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7166f3eb..bdbe279e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -57,7 +57,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: sea-orm-cli - version: 2.0.0-rc.17 + version: 2.0.0-rc.18 - name: Additional Integration tests working-directory: ./examples/sqlite run: | From 1b7bc8040b7a2a2966b54bb2c25eb5247f44333a Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 13 Nov 2025 23:14:43 +0000 Subject: [PATCH 65/85] sqlite: idempotent mutation test --- examples/sqlite/tests/mutation_tests.rs | 479 +++++++++++++++++------- 1 file changed, 340 insertions(+), 139 deletions(-) diff --git a/examples/sqlite/tests/mutation_tests.rs b/examples/sqlite/tests/mutation_tests.rs index 45573a9e..21ef28f3 100644 --- a/examples/sqlite/tests/mutation_tests.rs +++ b/examples/sqlite/tests/mutation_tests.rs @@ -1,6 +1,7 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; use seaography::async_graphql; +use serde::Deserialize; #[tokio::test] async fn main() { @@ -27,6 +28,16 @@ fn assert_eq(a: Response, b: &str) { async fn test_simple_insert_one() { let schema = schema().await; + schema + .execute( + r#" + mutation { + filmActorDelete(filter: { lastUpdate: { gt: "2022-11-14 10:30:12 UTC" } }) + } + "#, + ) + .await; + assert_eq( schema .execute( @@ -109,15 +120,45 @@ async fn test_simple_insert_one() { async fn test_complex_insert_one() { let schema = schema().await; - assert_eq( - schema - .execute( - r#" + schema + .execute( + r#" + mutation { + rentalDelete( + filter: { + rentalDate: { eq: "2030-01-25 21:50:05 UTC" } + inventoryId: { eq: 4452 } + customerId: { eq: 319 } + } + ) + } + "#, + ) + .await; + + #[derive(Deserialize)] + struct QueryResult { + rental: RentalQuery, + } + + #[derive(Deserialize)] + struct RentalQuery { + nodes: Vec, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct RentalId { + rental_id: i32, + } + + let response = schema + .execute( + r#" { - rental(filters: { rentalId: { eq: 16050 } }) { + rental(orderBy: { rentalId: DESC }) { nodes { rentalId - rentalDate inventoryId customerId returnDate @@ -126,101 +167,132 @@ async fn test_complex_insert_one() { } } "#, - ) - .await, - r#" - { - "rental": { - "nodes": [ - ] - } - } + ) + .await + .data + .into_json() + .unwrap(); + + let result: QueryResult = serde_json::from_value(response).unwrap(); + let max_id = result + .rental + .nodes + .iter() + .map(|r| r.rental_id) + .max() + .unwrap_or_default(); + let rental_id = max_id + 1; + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct CreateResult { + rental_create_one: RentalObject, + } + + #[derive(Debug, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + struct RentalObject { + rental_id: i32, + inventory_id: i32, + customer_id: i32, + staff_id: i32, + return_date: String, + } + + let response = schema + .execute(format!( + r#" + mutation {{ + rentalCreateOne( + data: {{ + rentalId: {rental_id} + rentalDate: "2030-01-25 21:50:05 UTC" + inventoryId: 4452 + customerId: 319 + returnDate: "2030-01-12 21:50:05 UTC" + staffId: 1 + lastUpdate: "2030-01-01 21:50:05 UTC" + }} + ) {{ + rentalId + inventoryId + customerId + returnDate + staffId + }} + }} "#, + )) + .await + .data + .into_json() + .unwrap(); + + let result: CreateResult = serde_json::from_value(response).unwrap(); + let rental = result.rental_create_one; + assert_eq!(rental.rental_id, rental_id); + assert_eq!( + rental, + RentalObject { + rental_id, + inventory_id: 4452, + customer_id: 319, + staff_id: 1, + return_date: "2030-01-12 21:50:05 UTC".into(), + } ); assert_eq( schema - .execute( + .execute(format!( r#" - mutation { - rentalCreateOne( - data: { - rentalId: 16050 - rentalDate: "2030-01-01 11:11:11 UTC" - inventoryId: 4452 - customerId: 319 - returnDate: "2030-01-01 11:11:11 UTC" - staffId: 1 - lastUpdate: "2030-01-01 11:11:11 UTC" - } - ) { + {{ + rental(filters: {{ rentalId: {{ eq: {rental_id} }} }}) {{ + nodes {{ rentalId - rentalDate inventoryId customerId - returnDate staffId - } - } - - "#, - ) - .await, - r#" - { - "rentalCreateOne": { - "rentalId": 16050, - "rentalDate": "2030-01-01 11:11:11 UTC", - "inventoryId": 4452, - "customerId": 319, - "returnDate": "2030-01-01 11:11:11 UTC", - "staffId": 1 - } - } - "#, - ); - - assert_eq( - schema - .execute( - r#" - { - rental(filters: { rentalId: { eq: 16050 } }) { - nodes { - rentalId - rentalDate - inventoryId - customerId - returnDate - staffId - } - } - } - "#, - ) + returnDate + }} + }} + }} + "# + )) .await, - r#" - { - "rental": { - "nodes": [ - { - "rentalId": 16050, - "rentalDate": "2030-01-01 11:11:11 UTC", - "inventoryId": 4452, - "customerId": 319, - "returnDate": "2030-01-01 11:11:11 UTC", - "staffId": 1 - } - ] - } - } - "#, + &format!( + r#" + {{ + "rental": {{ + "nodes": [ + {{ + "rentalId": {rental_id}, + "inventoryId": 4452, + "customerId": 319, + "staffId": 1, + "returnDate": "2030-01-12 21:50:05 UTC" + }} + ] + }} + }} + "# + ), ); } async fn test_create_batch_mutation() { let schema = schema().await; + schema + .execute( + r#" + mutation { + filmTextDelete(filter: { }) + } + "#, + ) + .await; + assert_eq( schema .execute( @@ -318,6 +390,16 @@ async fn test_create_batch_mutation() { } "#, ); + + schema + .execute( + r#" + mutation { + filmTextDelete(filter: { }) + } + "#, + ) + .await; } async fn test_update_mutation() { @@ -328,12 +410,12 @@ async fn test_update_mutation() { .execute( r#" { - country(pagination: { page: { limit: 10, page: 0 } }) { - nodes { - country - countryId - } + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId } + } } "#, ) @@ -365,22 +447,6 @@ async fn test_update_mutation() { { "country": "Argentina", "countryId": 6 - }, - { - "country": "Armenia", - "countryId": 7 - }, - { - "country": "Australia", - "countryId": 8 - }, - { - "country": "Austria", - "countryId": 9 - }, - { - "country": "Azerbaijan", - "countryId": 10 } ] } @@ -393,13 +459,13 @@ async fn test_update_mutation() { .execute( r#" mutation { - countryUpdate( - data: { country: "[DELETED]" } - filter: { countryId: { lt: 6 } } - ) { - countryId - country - } + countryUpdate( + data: { country: "[DELETED]" } + filter: { countryId: { lt: 6 } } + ) { + countryId + country + } } "#, ) @@ -437,12 +503,12 @@ async fn test_update_mutation() { .execute( r#" { - country(pagination: { page: { limit: 10, page: 0 } }) { - nodes { - country - countryId - } + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId } + } } "#, ) @@ -474,28 +540,48 @@ async fn test_update_mutation() { { "country": "Argentina", "countryId": 6 - }, - { - "country": "Armenia", - "countryId": 7 - }, - { - "country": "Australia", - "countryId": 8 - }, - { - "country": "Austria", - "countryId": 9 - }, - { - "country": "Azerbaijan", - "countryId": 10 } ] } } "#, ); + + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Afghanistan" } filter: { countryId: { eq: 1 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Algeria" } filter: { countryId: { eq: 2 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "American Samoa" } filter: { countryId: { eq: 3 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Angola" } filter: { countryId: { eq: 4 } }) { country } + }"#, + ) + .await; + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Anguilla" } filter: { countryId: { eq: 5 } }) { country } + }"#, + ) + .await; } async fn test_delete_mutation() { @@ -561,16 +647,12 @@ async fn test_delete_mutation() { .execute( r#" mutation { - filmTextDelete(filter: { filmId: { gte: 7 } }) + filmTextDelete(filter: { filmId: { gte: 7 } }) } "#, ) .await, - r#" - { - "filmTextDelete": 2 - } - "#, + r#"{ "filmTextDelete": 2 }"#, ); assert_eq( @@ -601,11 +683,63 @@ async fn test_delete_mutation() { } "#, ); + + assert_eq( + schema + .execute( + r#" + mutation { + filmTextDelete(filter: { filmId: { eq: 6 } }) + } + "#, + ) + .await, + r#"{ "filmTextDelete": 1 }"#, + ); } async fn test_add_original_language_to_film() { let schema = schema().await; + assert_eq( + schema + .execute( + r#" + { + film(filters: { filmId: { eq: 500 } }) { + nodes { + filmId + title + language1 { + name + } + language2 { + name + } + } + } + } + "#, + ) + .await, + r#" + { + "film": { + "nodes": [ + { + "filmId": 500, + "title": "KISS GLORY", + "language1": { + "name": "English" + }, + "language2": null + } + ] + } + } + "#, + ); + assert_eq( schema .execute( @@ -674,4 +808,71 @@ async fn test_add_original_language_to_film() { } "#, ); + + assert_eq( + schema + .execute( + r#" + mutation { + filmUpdate( + data: { originalLanguageId: null } + filter: { filmId: { eq: 500 } } + ) { + filmId + title + } + } + "#, + ) + .await, + r#" + { + "filmUpdate": [ + { + "filmId": 500, + "title": "KISS GLORY" + } + ] + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + film(filters: { filmId: { eq: 500 } }) { + nodes { + filmId + title + language1 { + name + } + language2 { + name + } + } + } + } + "#, + ) + .await, + r#" + { + "film": { + "nodes": [ + { + "filmId": 500, + "title": "KISS GLORY", + "language1": { + "name": "English" + }, + "language2": null + } + ] + } + } + "#, + ); } From 55c0fb6a86f970f3931e1fcb21ab60183bbcc06d Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 13 Nov 2025 23:35:08 +0000 Subject: [PATCH 66/85] Fix entity filter on update mutation Fixes #235 --- examples/sqlite/Cargo.toml | 1 + examples/sqlite/tests/entity_filter_tests.rs | 115 ++++++++++++++++++- examples/sqlite/tests/mutation_tests.rs | 5 + src/mutation/entity_update_mutation.rs | 35 +++--- 4 files changed, 141 insertions(+), 15 deletions(-) diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 9c7a7348..12dc2136 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -16,6 +16,7 @@ version = "~2.0.0-rc" features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] +path = "../../" version = "~2.0.0-rc.4" # seaography version features = ["with-decimal", "with-chrono"] diff --git a/examples/sqlite/tests/entity_filter_tests.rs b/examples/sqlite/tests/entity_filter_tests.rs index 0a370556..37f831fb 100644 --- a/examples/sqlite/tests/entity_filter_tests.rs +++ b/examples/sqlite/tests/entity_filter_tests.rs @@ -1,4 +1,4 @@ -use async_graphql::dynamic::*; +use async_graphql::{dynamic::*, Response}; use sea_orm::{ColumnTrait, Condition, Database}; use seaography::{ async_graphql, lazy_static, BuilderContext, LifecycleHooks, LifecycleHooksInterface, @@ -30,6 +30,7 @@ impl LifecycleHooksInterface for MyHooks { "Inventory" => Some(Condition::all().add(inventory::Column::StoreId.eq(2))), "Staff" => Some(Condition::all().add(staff::Column::StoreId.eq(2))), "Store" | "Stores" => Some(Condition::all().add(store::Column::StoreId.eq(2))), + "Country" => Some(Condition::all().add(country::Column::CountryId.eq(5))), _ => None, } } @@ -42,6 +43,13 @@ async fn schema() -> Schema { .unwrap() } +fn assert_eq(a: Response, b: &str) { + assert_eq!( + a.data.into_json().unwrap(), + serde_json::from_str::(b).unwrap() + ) +} + #[tokio::test] async fn only_store_2() { let schema = schema().await; @@ -205,3 +213,108 @@ async fn only_store_2() { }), ); } + +#[tokio::test] +#[cfg(not(feature = "field-pluralize"))] +async fn test_update_mutation_with_filter() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .init(); + + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "Anguilla", + "countryId": 5 + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + countryUpdate( + data: { country: "[DELETED]" } + filter: { countryId: { lt: 6 } } + ) { + countryId + country + } + } + "#, + ) + .await, + r#" + { + "countryUpdate": [ + { + "countryId": 5, + "country": "[DELETED]" + } + ] + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "[DELETED]", + "countryId": 5 + } + ] + } + } + "#, + ); + + schema + .execute( + r#"mutation { + countryUpdate(data: { country: "Anguilla" } filter: { countryId: { eq: 5 } }) { country } + }"#, + ) + .await; +} diff --git a/examples/sqlite/tests/mutation_tests.rs b/examples/sqlite/tests/mutation_tests.rs index 21ef28f3..d670e26e 100644 --- a/examples/sqlite/tests/mutation_tests.rs +++ b/examples/sqlite/tests/mutation_tests.rs @@ -5,6 +5,11 @@ use serde::Deserialize; #[tokio::test] async fn main() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .init(); + test_simple_insert_one().await; test_complex_insert_one().await; test_create_batch_mutation().await; diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs index 173a68ba..ce169a99 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -1,7 +1,7 @@ use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}; use sea_orm::{ - ActiveModelTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, - TransactionTrait, + ActiveModelTrait, ConnectionTrait, DatabaseConnection, EntityTrait, IntoActiveModel, + QueryFilter, QueryTrait, TransactionTrait, }; use crate::{ @@ -126,18 +126,25 @@ impl EntityUpdateMutationBuilder { input_object, )?; - let mut stmt = T::update_many().set(active_model); - if let Some(filter) = - hooks.entity_filter(&ctx, &object_name, OperationType::Update) - { - stmt = stmt.filter(filter); - } - stmt.filter(filter_condition.clone()) - .exec(&transaction) - .await?; - - let result: Vec = - T::find().filter(filter_condition).all(&transaction).await?; + let entity_filter = + hooks.entity_filter(&ctx, &object_name, OperationType::Update); + + let stmt = T::update_many() + .set(active_model) + .apply_if(entity_filter.clone(), |q, f| q.filter(f)) + .filter(filter_condition.clone()); + + let result: Vec = if db.support_returning() { + stmt.exec_with_returning(&transaction).await? + } else { + stmt.exec(&transaction).await?; + + T::find() + .apply_if(entity_filter, |q, f| q.filter(f)) + .filter(filter_condition) + .all(&transaction) + .await? + }; transaction.commit().await?; From 71d6ee18df08009186f7ec319b5a95560b6558ff Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 13 Nov 2025 23:44:03 +0000 Subject: [PATCH 67/85] Fix CI --- .github/workflows/tests.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index bdbe279e..e36b20cd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -86,9 +86,9 @@ jobs: -u sqlite://sakila.db seaography-sqlite-example -f poem - name: Depends on local seaography - run: >- - sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' - ./examples/sqlite/Cargo.toml + run: | + sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' ./examples/sqlite/Cargo.toml + sed -i '/^\[dev.dependencies\]$/a \serde = { version = "1", features = ["derive"] }' ./examples/sqlite/Cargo.toml - name: Build example working-directory: ./examples/sqlite run: cargo build @@ -152,9 +152,9 @@ jobs: -u mysql://sea:sea@127.0.0.1/sakila seaography-mysql-example -f actix - name: Depends on local seaography - run: >- - sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' - ./examples/mysql/Cargo.toml + run: | + sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' ./examples/mysql/Cargo.toml + sed -i '/^\[dev.dependencies\]$/a \serde = { version = "1", features = ["derive"] }' ./examples/mysql/Cargo.toml - name: Build example working-directory: ./examples/mysql run: cargo build From 6e881d708acdf1bd24ff368c099f16c427313171 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Fri, 14 Nov 2025 00:05:31 +0000 Subject: [PATCH 68/85] Disable async-graphql default features Fixes #236 --- Cargo.toml | 3 ++- examples/mysql/Cargo.toml | 2 +- examples/postgres/Cargo.toml | 2 +- examples/sqlite/Cargo.toml | 2 +- generator/src/templates/actix_cargo.toml | 2 +- generator/src/templates/axum_cargo.toml | 2 +- generator/src/templates/poem_cargo.toml | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b1b9245e..3cc9ba95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ features = ["default"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } +async-graphql = { version = "7.0", default-features = false, features = ["dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } seaography-macros = { version = "~2.0.0-rc.4", path = "macros", optional = true } itertools = { version = "0.12.0" } @@ -50,6 +50,7 @@ with-bigdecimal = ["sea-orm/with-bigdecimal", "async-graphql/bigdecimal"] with-postgres-array = ["sea-orm/postgres-array"] # with-ipnetwork = ["sea-orm/with-ipnetwork"] # with-mac_address = ["sea-orm/with-mac_address"] +graphql-playground = ["async-graphql/playground"] field-snake-case = [] field-camel-case = [] field-pluralize = ["pluralizer"] diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index 6c297c9d..e8814baa 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -18,7 +18,7 @@ features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" version = "~2.0.0-rc.4" # seaography version -features = ["with-decimal", "with-chrono"] +features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 7991da56..66bf54d2 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -18,7 +18,7 @@ features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" version = "~2.0.0-rc.4" # seaography version -features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] +features = ["graphql-playground", "with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] serde = { version = "1", features = ["derive"] } diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 12dc2136..b8d70898 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -18,7 +18,7 @@ features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" version = "~2.0.0-rc.4" # seaography version -features = ["with-decimal", "with-chrono"] +features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] serde = { version = "1", features = ["derive"] } diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index 69e02d5c..62cb9a1a 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -17,7 +17,7 @@ features = ["", "runtime-tokio-native-tls", "seaography" [dependencies.seaography] version = "~" # seaography version -features = ["with-decimal", "with-chrono"] +features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } diff --git a/generator/src/templates/axum_cargo.toml b/generator/src/templates/axum_cargo.toml index cd1e989d..931bfd03 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -17,7 +17,7 @@ features = ["", "runtime-tokio-native-tls", "seaography" [dependencies.seaography] version = "~" # seaography version -features = ["with-decimal", "with-chrono"] +features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } diff --git a/generator/src/templates/poem_cargo.toml b/generator/src/templates/poem_cargo.toml index 0414b360..4c776fce 100644 --- a/generator/src/templates/poem_cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -17,7 +17,7 @@ features = ["", "runtime-tokio-native-tls", "seaography" [dependencies.seaography] version = "~" # seaography version -features = ["with-decimal", "with-chrono"] +features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } From 96294a87e2f5cceb530e93c5c12f9565e839f076 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 24 Nov 2025 12:34:35 +0000 Subject: [PATCH 69/85] Bump sea-query --- src/query/loader.rs | 5 +--- src/query/loader/impl_traits.rs | 48 +++------------------------------ src/utilities.rs | 44 ++---------------------------- 3 files changed, 7 insertions(+), 90 deletions(-) diff --git a/src/query/loader.rs b/src/query/loader.rs index 7e0dcb43..c4721160 100644 --- a/src/query/loader.rs +++ b/src/query/loader.rs @@ -3,10 +3,7 @@ pub(crate) mod loader_impl; use loader_impl::*; -use sea_orm::{ - sea_query::{Value, ValueTuple}, - EntityTrait, QueryFilter, RelationDef, -}; +use sea_orm::{sea_query::ValueTuple, EntityTrait, QueryFilter, RelationDef}; use std::{collections::HashMap, hash::Hash, marker::PhantomData}; use crate::apply_order; diff --git a/src/query/loader/impl_traits.rs b/src/query/loader/impl_traits.rs index c586a877..24d7520f 100644 --- a/src/query/loader/impl_traits.rs +++ b/src/query/loader/impl_traits.rs @@ -5,9 +5,10 @@ where T: sea_orm::EntityTrait, { fn eq(&self, other: &Self) -> bool { - ValueTupleIter::new(&self.key) + self.key + .iter() .map(map_key) - .eq(ValueTupleIter::new(&other.key).map(map_key)) + .eq(other.key.iter().map(map_key)) && self.meta.eq(&other.meta) } } @@ -49,7 +50,7 @@ where T: sea_orm::EntityTrait, { fn hash(&self, state: &mut H) { - for key in ValueTupleIter::new(&self.key) { + for key in self.key.iter() { match key { sea_orm::Value::TinyInt(value) => { let value: Option = value.map(|value| value as i64); @@ -110,44 +111,3 @@ where format!("{:?}", self.order_by).hash(state); } } - -struct ValueTupleIter<'a> { - key: &'a ValueTuple, - index: usize, -} - -impl<'a> ValueTupleIter<'a> { - pub fn new(key: &'a ValueTuple) -> Self { - Self { key, index: 0 } - } -} - -impl<'a> Iterator for ValueTupleIter<'a> { - type Item = &'a Value; - - fn next(&mut self) -> Option { - let result = match self.key { - ValueTuple::One(iden1) => { - if self.index == 0 { - Some(iden1) - } else { - None - } - } - ValueTuple::Two(iden1, iden2) => match self.index { - 0 => Some(iden1), - 1 => Some(iden2), - _ => None, - }, - ValueTuple::Three(iden1, iden2, iden3) => match self.index { - 0 => Some(iden1), - 1 => Some(iden2), - 2 => Some(iden3), - _ => None, - }, - ValueTuple::Many(vec) => vec.get(self.index), - }; - self.index += 1; - result - } -} diff --git a/src/utilities.rs b/src/utilities.rs index 08680704..35e07669 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -5,7 +5,8 @@ use std::any::Any; /// used to encode the primary key values of a SeaORM entity to a String pub fn encode_cursor(values: ValueTuple) -> String { - ValueTupleIter::new(&values) + values + .iter() .map(|value| -> String { match value { sea_orm::Value::TinyInt(value) => { @@ -369,44 +370,3 @@ impl<'a> Iterator for IdenIter<'a> { result } } - -pub(crate) struct ValueTupleIter<'a> { - value: &'a ValueTuple, - index: usize, -} - -impl<'a> ValueTupleIter<'a> { - pub fn new(value: &'a ValueTuple) -> Self { - Self { value, index: 0 } - } -} - -impl<'a> Iterator for ValueTupleIter<'a> { - type Item = &'a sea_orm::Value; - - fn next(&mut self) -> Option { - let result = match self.value { - ValueTuple::One(a) => { - if self.index == 0 { - Some(a) - } else { - None - } - } - ValueTuple::Two(a, b) => match self.index { - 0 => Some(a), - 1 => Some(b), - _ => None, - }, - ValueTuple::Three(a, b, c) => match self.index { - 0 => Some(a), - 1 => Some(b), - 2 => Some(c), - _ => None, - }, - ValueTuple::Many(vec) => vec.get(self.index), - }; - self.index += 1; - result - } -} From 000da68d473c7c162d3f2a563428a6564649712f Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 24 Nov 2025 12:48:49 +0000 Subject: [PATCH 70/85] Bump actix --- examples/mysql/Cargo.toml | 2 +- generator/src/templates/actix_cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index e8814baa..3c4aae7c 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -4,7 +4,7 @@ name = "seaography-mysql-example" version = "2.0.0-rc.4" [dependencies] -actix-web = { version = "4.5", default-features = false, features = ["macros"] } +actix-web = { version = "4.11" } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index 62cb9a1a..5cb5a020 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -4,7 +4,7 @@ name = "" version = "0.1.0" [dependencies] -actix-web = { version = "4.5", default-features = false, features = ["macros"] } +actix-web = { version = "4.11" } async-graphql-actix-web = { version = "7.0" } dotenv = "0.15.0" tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } From cb4b9bf726abf61d1a5c7fe3c3df99cae5fa8dde Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 24 Nov 2025 20:50:05 +0000 Subject: [PATCH 71/85] Added before_active_model_save lifecycle hook fixes #238 --- examples/sqlite/tests/hook_guard_tests.rs | 125 ++++++++++++++++--- src/builder.rs | 2 +- src/builder_context/hooks.rs | 15 ++- src/mutation/entity_create_batch_mutation.rs | 13 +- src/mutation/entity_create_one_mutation.rs | 15 ++- src/mutation/entity_delete_mutation.rs | 2 +- src/mutation/entity_update_mutation.rs | 2 +- 7 files changed, 147 insertions(+), 27 deletions(-) diff --git a/examples/sqlite/tests/hook_guard_tests.rs b/examples/sqlite/tests/hook_guard_tests.rs index 9a99e249..10de5f2e 100644 --- a/examples/sqlite/tests/hook_guard_tests.rs +++ b/examples/sqlite/tests/hook_guard_tests.rs @@ -10,6 +10,7 @@ use seaography::{ use seaography_sqlite_example::entities::*; use serde_json::json; use std::{ + any::Any, collections::BTreeSet, sync::{Arc, Mutex}, }; @@ -28,6 +29,7 @@ enum HookCall { EntityGuard(String, OperationType), EntityWatch(String, OperationType), FieldGuard(String, Option, String, OperationType), + BeforeActiveModelSave(String, OperationType), } fn id_from_entity(entity: &str, value: &FieldValue<'_>) -> Option { @@ -62,6 +64,10 @@ impl HookCall { ) -> Self { HookCall::FieldGuard(entity.into(), id, field.into(), action) } + + fn before_active_model_save(entity: &str, action: OperationType) -> Self { + HookCall::BeforeActiveModelSave(entity.into(), action) + } } #[derive(Default, Clone)] @@ -83,13 +89,6 @@ impl Log { } fn calls(&self) -> Vec { - let mut calls = self.calls.lock().unwrap().clone(); - // Sort the list of calls so the tests don't depend on the order of field resolution - calls.sort(); - calls - } - - fn calls_unsorted(&self) -> Vec { self.calls.lock().unwrap().clone() } } @@ -165,6 +164,32 @@ impl LifecycleHooksInterface for MyHooks { _ => GuardAction::Allow, } } + + fn before_active_model_save( + &self, + ctx: &ResolverContext, + entity: &str, + action: OperationType, + active_model: &mut dyn Any, + ) -> GuardAction { + ctx.data::() + .unwrap() + .record(HookCall::before_active_model_save(entity, action)); + + match entity { + "FilmText" => { + let active_model: &film_text::ActiveModel = + active_model.downcast_ref().expect("Failed to downcast"); + if active_model.film_id != sea_orm::Set(5) { + // only allow inserting film_id = 5 + return GuardAction::Block(None); + } + } + _ => (), + } + + GuardAction::Allow + } } async fn schema(permissions: Permissions) -> (Schema, Log) { @@ -262,20 +287,20 @@ async fn entity_guard() { assert_eq!( log.calls(), vec![ - HookCall::entity_guard("FilmCategory", OperationType::Read), HookCall::entity_guard("Language", OperationType::Read), HookCall::field_guard("Language", None, "languageId", OperationType::Read), - HookCall::field_guard("Language", None, "languageId", OperationType::Read), - HookCall::field_guard("Language", None, "languageId", OperationType::Read), - HookCall::field_guard("Language", None, "languageId", OperationType::Read), - HookCall::field_guard("Language", None, "languageId", OperationType::Read), - HookCall::field_guard("Language", None, "languageId", OperationType::Read), HookCall::field_guard("Language", None, "name", OperationType::Read), + HookCall::field_guard("Language", None, "languageId", OperationType::Read), HookCall::field_guard("Language", None, "name", OperationType::Read), + HookCall::field_guard("Language", None, "languageId", OperationType::Read), HookCall::field_guard("Language", None, "name", OperationType::Read), + HookCall::field_guard("Language", None, "languageId", OperationType::Read), HookCall::field_guard("Language", None, "name", OperationType::Read), + HookCall::field_guard("Language", None, "languageId", OperationType::Read), HookCall::field_guard("Language", None, "name", OperationType::Read), + HookCall::field_guard("Language", None, "languageId", OperationType::Read), HookCall::field_guard("Language", None, "name", OperationType::Read), + HookCall::entity_guard("FilmCategory", OperationType::Read), ] ); } @@ -309,8 +334,8 @@ async fn field_guard() { vec![ HookCall::entity_guard("Language", OperationType::Read), HookCall::field_guard("Language", None, "languageId", OperationType::Read), - HookCall::field_guard("Language", None, "lastUpdate", OperationType::Read), HookCall::field_guard("Language", None, "name", OperationType::Read), + HookCall::field_guard("Language", None, "lastUpdate", OperationType::Read), ] ); } @@ -378,7 +403,7 @@ async fn entity_watch_mutation() { ); assert_eq!( - log.calls_unsorted(), + log.calls(), vec![ HookCall::entity_guard("Country", OperationType::Update), HookCall::field_guard("Country", None, "country", OperationType::Update), @@ -641,8 +666,8 @@ async fn entity_object_relation_not_owner2() { vec![ HookCall::entity_guard("Staff", OperationType::Read), HookCall::entity_guard("Staff", OperationType::Read), - HookCall::field_guard("Staff", Some(1), "firstName", OperationType::Read), HookCall::field_guard("Staff", Some(2), "selfRef", OperationType::Read), + HookCall::field_guard("Staff", Some(1), "firstName", OperationType::Read), ] ); } @@ -726,10 +751,10 @@ async fn entity_object_via_relation_owner1() { assert_eq!( log.calls(), vec![ - HookCall::entity_guard("Rental", OperationType::Read), HookCall::entity_guard("Staff", OperationType::Read), - HookCall::field_guard("Rental", Some(4), "rentalId", OperationType::Read), + HookCall::entity_guard("Rental", OperationType::Read), HookCall::field_guard("Staff", Some(2), "rental", OperationType::Read), + HookCall::field_guard("Rental", Some(4), "rentalId", OperationType::Read), ] ); } @@ -806,3 +831,67 @@ async fn entity_object_via_relation_not_owner2() { assert_eq!(response.errors[0].message, "Read on Staff(1).store denied"); assert_eq!(response.data.into_json().unwrap(), serde_json::Value::Null); } + +#[tokio::test] +async fn before_active_model_save() { + let permissions = Permissions::default(); + let (schema, log) = schema(permissions).await; + + let response = schema + .execute( + r#" + mutation { + filmTextCreateOne( + data: { filmId: 5, title: "Title for 5", description: "Description of 5" } + ) { + filmId + } + } + "#, + ) + .await; + + assert_eq!(response.errors.len(), 0); + assert_eq!( + log.calls(), + vec![ + HookCall::entity_guard("FilmText", OperationType::Create), + HookCall::field_guard("FilmText", None, "filmId", OperationType::Create), + HookCall::field_guard("FilmText", None, "title", OperationType::Create), + HookCall::field_guard("FilmText", None, "description", OperationType::Create), + HookCall::before_active_model_save("FilmText", OperationType::Create), + HookCall::entity_watch("FilmText", OperationType::Create), + HookCall::field_guard("FilmTextBasic", None, "filmId", OperationType::Read), + ] + ); + + schema + .execute( + r#" + mutation { + filmTextDelete(filter: { }) + } + "#, + ) + .await; + + let response = schema + .execute( + r#" + mutation { + filmTextCreateOne( + data: { filmId: 6, title: "Title for 6", description: "Description of 6" } + ) { + filmId + } + } + "#, + ) + .await; + + assert_eq!(response.errors.len(), 1); + assert_eq!( + response.errors[0].message, + "Blocked by before_active_model_save." + ); +} diff --git a/src/builder.rs b/src/builder.rs index aad2a76a..8bc0ab97 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -204,7 +204,7 @@ impl Builder { T: EntityTrait, ::Model: Sync, ::Model: IntoActiveModel
, - A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + Send + 'static, { let entity_object_builder = EntityObjectBuilder { context: self.context, diff --git a/src/builder_context/hooks.rs b/src/builder_context/hooks.rs index acb57196..671868fe 100644 --- a/src/builder_context/hooks.rs +++ b/src/builder_context/hooks.rs @@ -1,7 +1,7 @@ use super::GuardAction; use async_graphql::dynamic::ResolverContext; use sea_orm::{entity::prelude::async_trait, Condition}; -use std::ops::Deref; +use std::{any::Any, ops::Deref}; pub struct LifecycleHooks(pub(crate) Box); @@ -41,6 +41,7 @@ pub trait LifecycleHooksInterface: Send + Sync { /// This happens after an Entity is mutated async fn entity_watch(&self, _ctx: &ResolverContext, _entity: &str, _action: OperationType) {} + /// This happens before an Entity is accessed, invoked on each field fn field_guard( &self, _ctx: &ResolverContext, @@ -51,6 +52,7 @@ pub trait LifecycleHooksInterface: Send + Sync { GuardAction::Allow } + /// Apply custom filter to select, update and delete (but not insert) fn entity_filter( &self, _ctx: &ResolverContext, @@ -59,6 +61,17 @@ pub trait LifecycleHooksInterface: Send + Sync { ) -> Option { None } + + /// Inspect and modify an ActiveModel before save (only insert for now) + fn before_active_model_save( + &self, + _ctx: &ResolverContext, + _entity: &str, + _action: OperationType, + _active_model: &mut dyn Any, + ) -> GuardAction { + GuardAction::Allow + } } impl LifecycleHooksInterface for DefaultLifecycleHook {} diff --git a/src/mutation/entity_create_batch_mutation.rs b/src/mutation/entity_create_batch_mutation.rs index 4fc3cf65..b8d62daf 100644 --- a/src/mutation/entity_create_batch_mutation.rs +++ b/src/mutation/entity_create_batch_mutation.rs @@ -59,7 +59,7 @@ impl EntityCreateBatchMutationBuilder { T: EntityTrait, ::Model: Sync, ::Model: IntoActiveModel, - A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + Send + 'static, { let entity_input_builder = EntityInputBuilder { context: self.context, @@ -110,11 +110,20 @@ impl EntityCreateBatchMutationBuilder { } } - let active_model = prepare_active_model::( + let mut active_model = prepare_active_model::( &entity_input_builder, &entity_object_builder, input_object, )?; + if let GuardAction::Block(reason) = hooks.before_active_model_save( + &ctx, + &object_name, + OperationType::Create, + &mut active_model, + ) { + return Err(guard_error(reason, "Blocked by before_active_model_save.")); + } + let result = active_model.insert(&transaction).await?; results.push(result); } diff --git a/src/mutation/entity_create_one_mutation.rs b/src/mutation/entity_create_one_mutation.rs index 4f33d783..58321156 100644 --- a/src/mutation/entity_create_one_mutation.rs +++ b/src/mutation/entity_create_one_mutation.rs @@ -60,7 +60,7 @@ impl EntityCreateOneMutationBuilder { T: EntityTrait, ::Model: Sync, ::Model: IntoActiveModel, - A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + Send + 'static, { let entity_input_builder = EntityInputBuilder { context: self.context, @@ -105,12 +105,21 @@ impl EntityCreateOneMutationBuilder { .data::()? .restricted(ctx.data_opt::())?; - let active_model = prepare_active_model::( + let mut active_model = prepare_active_model::( &entity_input_builder, &entity_object_builder, input_object, )?; + if let GuardAction::Block(reason) = hooks.before_active_model_save( + &ctx, + &object_name, + OperationType::Create, + &mut active_model, + ) { + return Err(guard_error(reason, "Blocked by before_active_model_save.")); + } + let result = active_model.insert(db).await?; hooks @@ -136,7 +145,7 @@ pub fn prepare_active_model( where T: EntityTrait, ::Model: IntoActiveModel, - A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + Send, { let mut data = entity_input_builder.parse_object::(input_object)?; diff --git a/src/mutation/entity_delete_mutation.rs b/src/mutation/entity_delete_mutation.rs index 9ff3ef62..36b0073e 100644 --- a/src/mutation/entity_delete_mutation.rs +++ b/src/mutation/entity_delete_mutation.rs @@ -59,7 +59,7 @@ impl EntityDeleteMutationBuilder { where T: EntityTrait, ::Model: IntoActiveModel, - A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + Send, { let entity_filter_input_builder = FilterInputBuilder { context: self.context, diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs index ce169a99..0849d02c 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -66,7 +66,7 @@ impl EntityUpdateMutationBuilder { T: EntityTrait, ::Model: Sync, ::Model: IntoActiveModel, - A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + Send, { let entity_input_builder = EntityInputBuilder { context: self.context, From 9f073b93ef97800537dfc18761eaac0968ce4e92 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 28 Dec 2025 20:11:24 +0000 Subject: [PATCH 72/85] Update sea-query & sea-orm --- examples/postgres/tests/mutation_tests.rs | 6 ++++ src/builder_context/types_map.rs | 32 ++++++++++++-------- src/mutation/entity_create_batch_mutation.rs | 5 ++- src/outputs/entity_object.rs | 19 +++++++----- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/examples/postgres/tests/mutation_tests.rs b/examples/postgres/tests/mutation_tests.rs index b754ead0..68939030 100644 --- a/examples/postgres/tests/mutation_tests.rs +++ b/examples/postgres/tests/mutation_tests.rs @@ -466,6 +466,8 @@ async fn test_create_batch_mutation() { async fn test_update_mutation() { let schema = schema().await; + restore_country(&schema).await; + assert_eq( schema .execute( @@ -608,6 +610,10 @@ async fn test_update_mutation() { "#, ); + restore_country(&schema).await; +} + +async fn restore_country(schema: &Schema) { schema .execute( r#"mutation { diff --git a/src/builder_context/types_map.rs b/src/builder_context/types_map.rs index e8f58bee..8273cb86 100644 --- a/src/builder_context/types_map.rs +++ b/src/builder_context/types_map.rs @@ -852,16 +852,22 @@ pub fn converted_value_to_sea_orm_value( } #[cfg(feature = "with-postgres-array")] ConvertedType::Array(ty) => { - let list_value = value - .list()? - .iter() - .map(|value| converted_value_to_sea_orm_value(ty, &value, entity_name, column_name)) - .collect::>>()?; - - sea_orm::Value::Array( - converted_type_to_sea_orm_array_type(ty)?, - Some(Box::new(list_value)), - ) + let list_value = value.list()?; + + if list_value.is_empty() { + sea_orm::Value::Array(sea_orm::sea_query::Array::Null( + converted_type_to_sea_orm_array_type(ty)?, + )) + } else { + let list_value = list_value + .iter() + .map(|value| { + converted_value_to_sea_orm_value(ty, &value, entity_name, column_name) + }) + .collect::>>()?; + + sea_orm::Value::Array(list_value.into()) + } } // FIXME: support ip type // #[cfg(feature = "with-ipnetwork")] // ConvertedType::IpNetwork => { @@ -926,9 +932,9 @@ pub fn converted_null_to_sea_orm_value(column_type: &ConvertedType) -> SeaResult #[cfg(feature = "with-bigdecimal")] ConvertedType::BigDecimal => sea_orm::Value::BigDecimal(None), #[cfg(feature = "with-postgres-array")] - ConvertedType::Array(ty) => { - sea_orm::Value::Array(converted_type_to_sea_orm_array_type(ty)?, None) - } + ConvertedType::Array(ty) => sea_orm::Value::Array(sea_orm::sea_query::Array::Null( + converted_type_to_sea_orm_array_type(ty)?, + )), }) } diff --git a/src/mutation/entity_create_batch_mutation.rs b/src/mutation/entity_create_batch_mutation.rs index b8d62daf..b0765319 100644 --- a/src/mutation/entity_create_batch_mutation.rs +++ b/src/mutation/entity_create_batch_mutation.rs @@ -121,7 +121,10 @@ impl EntityCreateBatchMutationBuilder { OperationType::Create, &mut active_model, ) { - return Err(guard_error(reason, "Blocked by before_active_model_save.")); + return Err(guard_error( + reason, + "Blocked by before_active_model_save.", + )); } let result = active_model.insert(&transaction).await?; diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index dca74872..73f8af27 100644 --- a/src/outputs/entity_object.rs +++ b/src/outputs/entity_object.rs @@ -268,16 +268,19 @@ pub(crate) fn sea_query_value_to_graphql_value( sea_orm::Value::Bytes(value) => value.map(|it| Value::from(String::from_utf8_lossy(&it))), #[cfg(feature = "with-postgres-array")] - sea_orm::Value::Array(_array_value, value) => value.map(|it| { - Value::List( - it.into_iter() - .map(|item| { - sea_query_value_to_graphql_value(_context, item, is_enum) - .unwrap_or(Value::Null) + sea_orm::Value::Array(value) => match value { + sea_orm::sea_query::Array::Null(_) => None, + _ => Some(Value::List( + value + .iter_value() + .map(|item| match item { + Some(v) => sea_query_value_to_graphql_value(_context, v, is_enum) + .unwrap_or(Value::Null), + None => Value::Null, }) .collect(), - ) - }), + )), + }, #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] From d6524a601d724737b1511d594035d36bcebb0ad3 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 28 Dec 2025 21:42:48 +0000 Subject: [PATCH 73/85] 2.0.0-rc.6 --- Cargo.toml | 4 ++-- cli/Cargo.toml | 4 ++-- generator/Cargo.toml | 2 +- macros/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3cc9ba95..fb5c6dec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" edition = "2021" rust-version = "1.70" authors = [ @@ -25,7 +25,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] async-graphql = { version = "7.0", default-features = false, features = ["dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } -seaography-macros = { version = "~2.0.0-rc.4", path = "macros", optional = true } +seaography-macros = { version = "~2.0.0-rc.6", path = "macros", optional = true } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ac6da0b7..80693c5e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-cli" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } clap = { version = "4.3.19", features = ["env", "derive"] } -seaography-generator = { version = "~2.0.0-rc.4", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc.6", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/generator/Cargo.toml b/generator/Cargo.toml index c09d3848..1517e709 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-generator" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index b7352b49..a1cf78ba 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-macros" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" edition = "2024" rust-version = "1.85" authors = ["Chris Tsang "] From dbb6b4d883437d6637f9e04fe66c7a369bcc9a73 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 28 Dec 2025 21:42:48 +0000 Subject: [PATCH 74/85] update examples --- examples/mysql/Cargo.toml | 4 ++-- examples/postgres/Cargo.toml | 4 ++-- examples/sea-draw/Cargo.toml | 4 ++-- examples/sqlite/Cargo.toml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index 3c4aae7c..9d3c909d 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-mysql-example" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" [dependencies] actix-web = { version = "4.11" } @@ -17,7 +17,7 @@ features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.4" # seaography version +version = "~2.0.0-rc.6" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 66bf54d2..18b92a19 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" [dependencies] axum = { version = "0.8" } @@ -17,7 +17,7 @@ features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.4" # seaography version +version = "~2.0.0-rc.6" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index 576b5f44..e01bc0c9 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-draw" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" edition = "2024" [dependencies] @@ -30,7 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.4" # seaography version +version = "~2.0.0-rc.6" # seaography version features = [ "macros", "field-pluralize", diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index b8d70898..4734ee9b 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "2.0.0-rc.4" +version = "2.0.0-rc.6" [dependencies] poem = { version = "3.0" } @@ -17,7 +17,7 @@ features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.4" # seaography version +version = "~2.0.0-rc.6" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] From a1cb196b51d682d2e7462c6027929bff034d5a3d Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 29 Dec 2025 01:18:43 +0000 Subject: [PATCH 75/85] clippy --- src/inputs/active_enum_filter_input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inputs/active_enum_filter_input.rs b/src/inputs/active_enum_filter_input.rs index cbb5f837..affce243 100644 --- a/src/inputs/active_enum_filter_input.rs +++ b/src/inputs/active_enum_filter_input.rs @@ -94,7 +94,7 @@ where let extract_variant = |input: &str| -> SeaResult { let variant = variants.iter().find(|variant| { - let variant = format_variant(&variant.to_string()); + let variant = format_variant(variant.as_ref()); variant.eq(input) }); Ok(variant From 99f886bc44cda532350bae60691f506f41f73f0e Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 29 Dec 2025 19:01:33 +0000 Subject: [PATCH 76/85] Restore value system --- src/builder_context/types_map.rs | 32 +++++++++++++------------------- src/outputs/entity_object.rs | 19 ++++++++----------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/builder_context/types_map.rs b/src/builder_context/types_map.rs index 8273cb86..e8f58bee 100644 --- a/src/builder_context/types_map.rs +++ b/src/builder_context/types_map.rs @@ -852,22 +852,16 @@ pub fn converted_value_to_sea_orm_value( } #[cfg(feature = "with-postgres-array")] ConvertedType::Array(ty) => { - let list_value = value.list()?; - - if list_value.is_empty() { - sea_orm::Value::Array(sea_orm::sea_query::Array::Null( - converted_type_to_sea_orm_array_type(ty)?, - )) - } else { - let list_value = list_value - .iter() - .map(|value| { - converted_value_to_sea_orm_value(ty, &value, entity_name, column_name) - }) - .collect::>>()?; - - sea_orm::Value::Array(list_value.into()) - } + let list_value = value + .list()? + .iter() + .map(|value| converted_value_to_sea_orm_value(ty, &value, entity_name, column_name)) + .collect::>>()?; + + sea_orm::Value::Array( + converted_type_to_sea_orm_array_type(ty)?, + Some(Box::new(list_value)), + ) } // FIXME: support ip type // #[cfg(feature = "with-ipnetwork")] // ConvertedType::IpNetwork => { @@ -932,9 +926,9 @@ pub fn converted_null_to_sea_orm_value(column_type: &ConvertedType) -> SeaResult #[cfg(feature = "with-bigdecimal")] ConvertedType::BigDecimal => sea_orm::Value::BigDecimal(None), #[cfg(feature = "with-postgres-array")] - ConvertedType::Array(ty) => sea_orm::Value::Array(sea_orm::sea_query::Array::Null( - converted_type_to_sea_orm_array_type(ty)?, - )), + ConvertedType::Array(ty) => { + sea_orm::Value::Array(converted_type_to_sea_orm_array_type(ty)?, None) + } }) } diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index 73f8af27..dca74872 100644 --- a/src/outputs/entity_object.rs +++ b/src/outputs/entity_object.rs @@ -268,19 +268,16 @@ pub(crate) fn sea_query_value_to_graphql_value( sea_orm::Value::Bytes(value) => value.map(|it| Value::from(String::from_utf8_lossy(&it))), #[cfg(feature = "with-postgres-array")] - sea_orm::Value::Array(value) => match value { - sea_orm::sea_query::Array::Null(_) => None, - _ => Some(Value::List( - value - .iter_value() - .map(|item| match item { - Some(v) => sea_query_value_to_graphql_value(_context, v, is_enum) - .unwrap_or(Value::Null), - None => Value::Null, + sea_orm::Value::Array(_array_value, value) => value.map(|it| { + Value::List( + it.into_iter() + .map(|item| { + sea_query_value_to_graphql_value(_context, item, is_enum) + .unwrap_or(Value::Null) }) .collect(), - )), - }, + ) + }), #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] From 1e7dea79405b7b4c73fb316d822a4844b5586684 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 29 Dec 2025 20:24:18 +0000 Subject: [PATCH 77/85] Fix --- src/inputs/active_enum_filter_input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inputs/active_enum_filter_input.rs b/src/inputs/active_enum_filter_input.rs index affce243..a3a2b54e 100644 --- a/src/inputs/active_enum_filter_input.rs +++ b/src/inputs/active_enum_filter_input.rs @@ -94,7 +94,7 @@ where let extract_variant = |input: &str| -> SeaResult { let variant = variants.iter().find(|variant| { - let variant = format_variant(variant.as_ref()); + let variant = format_variant(&variant.inner()); variant.eq(input) }); Ok(variant From 0435d108d5407d7d11cc846a50601ebfc8af3eb7 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 7 Jan 2026 14:40:07 +0000 Subject: [PATCH 78/85] Fix and clippy --- macros/src/convert_output.rs | 2 +- src/builder_context/types_map.rs | 33 +++++++++++++++----------------- src/custom/types.rs | 13 ++++--------- src/outputs/entity_object.rs | 2 +- 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/macros/src/convert_output.rs b/macros/src/convert_output.rs index b748c49f..b65f5037 100644 --- a/macros/src/convert_output.rs +++ b/macros/src/convert_output.rs @@ -77,7 +77,7 @@ fn derive_convert_output_enum_containers( ) -> async_graphql::Result>> { if let sea_orm::sea_query::value::Value::Json(opt_json) = value { if let Some(json) = opt_json { - match serde_json::from_value::<#orig_ident>(json.clone()) { + match serde_json::from_value::<#orig_ident>(*json.clone()) { Ok(obj) => match obj { #(#variant_matches)* }, diff --git a/src/builder_context/types_map.rs b/src/builder_context/types_map.rs index e8f58bee..70467756 100644 --- a/src/builder_context/types_map.rs +++ b/src/builder_context/types_map.rs @@ -655,12 +655,12 @@ pub fn converted_value_to_sea_orm_value( Ok(s) => sea_orm::entity::prelude::Json::from_str(s).map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("Json - {}.{}", entity_name, column_name), + format!("Json - {entity_name}.{column_name}"), ) })?, Err(_) => value.deserialize()?, }; - sea_orm::Value::Json(Some(value)) + sea_orm::Value::Json(Some(Box::new(value))) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDate => { @@ -669,7 +669,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("ChronoDate - {}.{}", entity_name, column_name), + format!("ChronoDate - {entity_name}.{column_name}"), ) })?; @@ -682,7 +682,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("ChronoTime - {}.{}", entity_name, column_name), + format!("ChronoTime - {entity_name}.{column_name}"), ) })?; @@ -697,7 +697,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("ChronoDateTime - {}.{}", entity_name, column_name), + format!("ChronoDateTime - {entity_name}.{column_name}"), ) })?; @@ -711,7 +711,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("ChronoDateTimeUtc - {}.{}", entity_name, column_name), + format!("ChronoDateTimeUtc - {entity_name}.{column_name}"), ) })?; @@ -725,7 +725,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("ChronoDateTimeLocal - {}.{}", entity_name, column_name), + format!("ChronoDateTimeLocal - {entity_name}.{column_name}"), ) })?; @@ -740,10 +740,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!( - "ChronoDateTimeWithTimeZone - {}.{}", - entity_name, column_name - ), + format!("ChronoDateTimeWithTimeZone - {entity_name}.{column_name}",), ) })?; @@ -758,7 +755,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("TimeDate - {}.{}", entity_name, column_name), + format!("TimeDate - {entity_name}.{column_name}"), ) })?; @@ -773,7 +770,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("TimeTime - {}.{}", entity_name, column_name), + format!("TimeTime - {entity_name}.{column_name}"), ) })?; @@ -788,7 +785,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("TimeDateTime - {}.{}", entity_name, column_name), + format!("TimeDateTime - {entity_name}.{column_name}"), ) })?; @@ -803,7 +800,7 @@ pub fn converted_value_to_sea_orm_value( .map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("TimeDateTimeWithTimeZone - {}.{}", entity_name, column_name), + format!("TimeDateTimeWithTimeZone - {entity_name}.{column_name}"), ) })?; @@ -816,7 +813,7 @@ pub fn converted_value_to_sea_orm_value( let value = sea_orm::entity::prelude::Uuid::from_str(value.string()?).map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("Uuid - {}.{}", entity_name, column_name), + format!("Uuid - {entity_name}.{column_name}"), ) })?; @@ -830,7 +827,7 @@ pub fn converted_value_to_sea_orm_value( sea_orm::entity::prelude::Decimal::from_str(value.string()?).map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("Decimal - {}.{}", entity_name, column_name), + format!("Decimal - {entity_name}.{column_name}"), ) })?; @@ -844,7 +841,7 @@ pub fn converted_value_to_sea_orm_value( sea_orm::entity::prelude::BigDecimal::from_str(value.string()?).map_err(|e| { crate::SeaographyError::TypeConversionError( e.to_string(), - format!("BigDecimal - {}.{}", entity_name, column_name), + format!("BigDecimal - {entity_name}.{column_name}"), ) })?; diff --git a/src/custom/types.rs b/src/custom/types.rs index dc2adea5..99656cfc 100644 --- a/src/custom/types.rs +++ b/src/custom/types.rs @@ -127,18 +127,13 @@ where let column_type = types_map_helper.sea_orm_column_type_to_converted_type(None, &column_type); - if value.is_none() { + if let Some(value) = value { + let value = converted_value_to_sea_orm_value(&column_type, &value, "", "")?; // this is Value::unwrap and should not panic - Ok(converted_null_to_sea_orm_value(&column_type)?.unwrap()) + Ok(value.unwrap()) } else { - let value = converted_value_to_sea_orm_value( - &column_type, - &value.expect("Checked not null"), - "", - "", - )?; // this is Value::unwrap and should not panic - Ok(value.unwrap()) + Ok(converted_null_to_sea_orm_value(&column_type)?.unwrap()) } } diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index dca74872..4391874f 100644 --- a/src/outputs/entity_object.rs +++ b/src/outputs/entity_object.rs @@ -282,7 +282,7 @@ pub(crate) fn sea_query_value_to_graphql_value( #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] sea_orm::sea_query::Value::Json(value) => { - value.map(|it| match Value::from_json(it.clone()) { + value.map(|it| match Value::from_json(*it.clone()) { Ok(v) => v, Err(_) => Value::from(it.to_string()), }) From 9db261f7572f80409991a842503ca9ad637d16a1 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 7 Jan 2026 14:58:01 +0000 Subject: [PATCH 79/85] Fix sea-draw --- examples/sea-draw/Cargo.toml | 2 +- examples/sea-draw/src/subscriptions.rs | 44 +++++++++++++------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index e01bc0c9..7cdf85ef 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0-rc.6" edition = "2024" [dependencies] -async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } +async-graphql = { version = "7.0", features = ["dataloader", "dynamic-schema"] } async-graphql-axum = { version = "7" } axum = { version = "0.8.*", features = ["ws"] } clap = { version = "4.5", features = ["derive", "env"] } diff --git a/examples/sea-draw/src/subscriptions.rs b/examples/sea-draw/src/subscriptions.rs index 581d2917..b39cb247 100644 --- a/examples/sea-draw/src/subscriptions.rs +++ b/examples/sea-draw/src/subscriptions.rs @@ -30,27 +30,27 @@ impl Subscription { _ctx: ResolverContext<'_>, _id: Uuid, ) -> async_graphql::Result>>> { - Ok(async_graphql::async_stream::stream! { - yield Ok(FieldValue::owned_any("test".to_string())); - - // while let Some(msg) = rx.recv().await { - // tracing::info!( - // chat_id = id.to_string(), - // project_id = chat.project_id.to_string(), - // account_id = access.account_id().to_string(), - // message_id = msg.id.to_string(), - // "Sending message for chat" - // ); - // yield Ok(FieldValue::owned_any(msg)); - // } - - // multiplexer.remove_chat(id, listener_id); - // tracing::info!( - // chat_id = id.to_string(), - // project_id = chat.project_id.to_string(), - // account_id = access.account_id().to_string(), - // "Removed listener for chat" - // ); - }) + Ok(tokio_stream::once(Ok(FieldValue::owned_any( + "test".to_string(), + )))) + // Ok(async_graphql::async_stream::stream! { + // while let Some(msg) = rx.recv().await { + // tracing::info!( + // chat_id = id.to_string(), + // project_id = chat.project_id.to_string(), + // account_id = access.account_id().to_string(), + // message_id = msg.id.to_string(), + // "Sending message for chat" + // ); + // yield Ok(FieldValue::owned_any(msg)); + // } + // multiplexer.remove_chat(id, listener_id); + // tracing::info!( + // chat_id = id.to_string(), + // project_id = chat.project_id.to_string(), + // account_id = access.account_id().to_string(), + // "Removed listener for chat" + // ); + // }) } } From ee380956016e74531015051a31f889e45337a335 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Wed, 7 Jan 2026 15:51:56 +0000 Subject: [PATCH 80/85] Try fix CI --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fb5c6dec..8196acc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ features = ["default"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -async-graphql = { version = "7.0", default-features = false, features = ["dataloader", "dynamic-schema"] } +async-graphql = { version = "~7.0", default-features = false, features = ["dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } seaography-macros = { version = "~2.0.0-rc.6", path = "macros", optional = true } itertools = { version = "0.12.0" } From c4ae981a4603818cc7664cbfa47e208c73309edf Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sat, 10 Jan 2026 11:55:20 +0000 Subject: [PATCH 81/85] Precise version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8196acc3..2c4ac251 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ features = ["default"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -async-graphql = { version = "~7.0", default-features = false, features = ["dataloader", "dynamic-schema"] } +async-graphql = { version = "~7.0.17", default-features = false, features = ["dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } seaography-macros = { version = "~2.0.0-rc.6", path = "macros", optional = true } itertools = { version = "0.12.0" } From f58771f0350a979ee376de166e023ddb2a8ab9ed Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 11 Jan 2026 22:55:34 +0000 Subject: [PATCH 82/85] 2.0.0-rc.8 --- Cargo.toml | 4 ++-- cli/Cargo.toml | 4 ++-- generator/Cargo.toml | 2 +- macros/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c4ac251..7bc64c63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" edition = "2021" rust-version = "1.70" authors = [ @@ -25,7 +25,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] async-graphql = { version = "~7.0.17", default-features = false, features = ["dataloader", "dynamic-schema"] } sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } -seaography-macros = { version = "~2.0.0-rc.6", path = "macros", optional = true } +seaography-macros = { version = "~2.0.0-rc.8", path = "macros", optional = true } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 80693c5e..efef2656 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-cli" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } clap = { version = "4.3.19", features = ["env", "derive"] } -seaography-generator = { version = "~2.0.0-rc.6", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc.8", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/generator/Cargo.toml b/generator/Cargo.toml index 1517e709..fecd271c 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-generator" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index a1cf78ba..cd9c593b 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-macros" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" edition = "2024" rust-version = "1.85" authors = ["Chris Tsang "] From eff4897b8664bdf56f655214e7e58c4b2c41cce3 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 11 Jan 2026 22:55:34 +0000 Subject: [PATCH 83/85] update examples --- examples/mysql/Cargo.toml | 4 ++-- examples/postgres/Cargo.toml | 4 ++-- examples/sea-draw/Cargo.toml | 4 ++-- examples/sqlite/Cargo.toml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index 9d3c909d..692901b9 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-mysql-example" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" [dependencies] actix-web = { version = "4.11" } @@ -17,7 +17,7 @@ features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.6" # seaography version +version = "~2.0.0-rc.8" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 18b92a19..88ee9473 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" [dependencies] axum = { version = "0.8" } @@ -17,7 +17,7 @@ features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.6" # seaography version +version = "~2.0.0-rc.8" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index 7cdf85ef..16addb02 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-draw" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" edition = "2024" [dependencies] @@ -30,7 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.6" # seaography version +version = "~2.0.0-rc.8" # seaography version features = [ "macros", "field-pluralize", diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 4734ee9b..db9635a9 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "2.0.0-rc.6" +version = "2.0.0-rc.8" [dependencies] poem = { version = "3.0" } @@ -17,7 +17,7 @@ features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.6" # seaography version +version = "~2.0.0-rc.8" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] From 989723d9f113345f9f3c7b9ad30c7ab53e876dd8 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 12 Jan 2026 21:30:29 +0000 Subject: [PATCH 84/85] Trigger after_save after model update --- src/mutation/entity_update_mutation.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs index 0849d02c..1fd67c3f 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -146,6 +146,10 @@ impl EntityUpdateMutationBuilder { .await? }; + for model in result.iter() { + A::after_save(model.clone(), &transaction, false).await?; + } + transaction.commit().await?; hooks From d530a9ff801e51cec4bf82bebb9e68a5af977bf2 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Thu, 18 Jun 2026 13:02:17 +0100 Subject: [PATCH 85/85] 2.0.0-rc.9 --- CHANGELOG.md | 4 + Cargo.toml | 6 +- RELEASE.md | 143 +++++++++++++++++++++++ cli/Cargo.toml | 4 +- examples/mysql/Cargo.toml | 6 +- examples/postgres/Cargo.toml | 6 +- examples/sea-draw/Cargo.toml | 6 +- examples/sqlite/Cargo.toml | 6 +- generator/Cargo.toml | 2 +- generator/src/templates/actix_cargo.toml | 2 +- generator/src/templates/axum_cargo.toml | 2 +- generator/src/templates/poem_cargo.toml | 2 +- macros/Cargo.toml | 2 +- 13 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 RELEASE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f917bc50..247ec06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). } ``` +### Bug Fixes + +* Trigger the `after_save` hook after update mutations + ## 1.1.5 - pending ### New Features diff --git a/Cargo.toml b/Cargo.toml index 7bc64c63..97d2bb28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" edition = "2021" rust-version = "1.70" authors = [ @@ -24,8 +24,8 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] async-graphql = { version = "~7.0.17", default-features = false, features = ["dataloader", "dynamic-schema"] } -sea-orm = { version = "~2.0.0-rc", default-features = false, features = ["seaography", "with-json"] } -seaography-macros = { version = "~2.0.0-rc.8", path = "macros", optional = true } +sea-orm = { version = "~2.0.0-rc.40", default-features = false, features = ["seaography", "with-json"] } +seaography-macros = { version = "~2.0.0-rc.9", path = "macros", optional = true } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..43273cce --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,143 @@ +# Release Process + +This is the release checklist for Seaography release candidates. Seaography +tracks SeaORM's `2.0.0-rc.N` cadence and is published together with it. + +## 1. Choose Version + +Check the latest published versions: + +```sh +cargo search seaography --limit 1 +cargo search sea-orm --limit 1 +``` + +Seaography has its own `2.0.0-rc.N` counter (independent of SeaORM's). Bump to the +next `2.0.0-rc.N`. Note the SeaORM rc you are releasing against (e.g. `2.0.0-rc.40`); +that becomes the dependency floor (see step 3). + +## 2. Start Clean + +Release from `main` after the change PRs have been merged and CI has passed. + +```sh +git switch main +git pull --ff-only +git status --short +``` + +The worktree should be clean before starting. + +## 3. Bump Versions + +There is no bump script; update each location by hand. The four publishable crates +are `seaography`, `seaography-macros`, `seaography-cli`, and `seaography-generator`. + +1. Crate versions — set `version` in each: + - `Cargo.toml` (`seaography`) + - `macros/Cargo.toml` (`seaography-macros`) + - `generator/Cargo.toml` (`seaography-generator`) + - `cli/Cargo.toml` (`seaography-cli`) + +2. Internal dependency pins — bump to the new version: + - `Cargo.toml`: `seaography-macros = { version = "~2.0.0-rc.N", ... }` + - `cli/Cargo.toml`: `seaography-generator = { version = "~2.0.0-rc.N", ... }` + +3. SeaORM dependency floor — set to the SeaORM rc you are releasing against. The + `~2.0.0-rc.N` form floats forward to later rc's and bridges to `2.0.x` stable: + - `Cargo.toml`: `sea-orm = { version = "~2.0.0-rc.N", ... }` + - Example crates: `examples/*/Cargo.toml` (`sea-orm` line, plus the + `# seaography version` pin). + +4. Code generator templates — the generated project's `Cargo.toml` hardcodes the + SeaORM pin; bump it in all three framework templates: + - `generator/src/templates/poem_cargo.toml` + - `generator/src/templates/actix_cargo.toml` + - `generator/src/templates/axum_cargo.toml` + + (The generated `seaography` pin is injected from `CARGO_PKG_VERSION` via + `generator/src/writer.rs`, so it tracks the bump automatically — no edit needed.) + +Sanity check that nothing was missed: + +```sh +grep -rn '2.0.0-rc' --include=Cargo.toml . | grep -v '#' +``` + +## 4. Write Changelog + +The 2.0 line uses a single rolling section, `## 2.0.0 - pending`, in `CHANGELOG.md` +— **not** per-rc sections. Add entries (New Features / Enhancements / Bug Fixes) +under it. Leave the `pending` heading as-is; it is only renamed to +`## 2.0.0 - YYYY-MM-DD` at the 2.0.0 final release. + +## 5. Validate Locally + +```sh +cargo check --workspace --all-features +``` + +To validate against an unpublished local SeaORM (e.g. the matching rc not yet on +crates.io), temporarily add a patch, check, then revert: + +```toml +[patch.crates-io] +sea-orm = { path = "../sea-orm" } +``` + +Build the examples if backend behavior changed. + +## 6. Push and Wait for CI + +```sh +git commit -am "2.0.0-rc.N" +git push origin main +``` + +Wait for GitHub Actions to pass before publishing. Do not publish while CI is red. + +## 7. Publish Crates + +Publish in dependency order (dependencies before dependents): + +1. `seaography-macros` +2. `seaography` (depends on `seaography-macros`) +3. `seaography-generator` +4. `seaography-cli` (depends on `seaography-generator`) + +```sh +(cd macros && cargo publish) +cargo publish +(cd generator && cargo publish) +(cd cli && cargo publish) +``` + +If crates.io indexing causes a dependent publish to fail, wait briefly and retry +the failed crate. The SeaORM rc you depend on must already be published. + +## 8. Tag and GitHub Release + +```sh +git tag 2.0.0-rc.N +git push origin 2.0.0-rc.N +``` + +Create a GitHub Release describing the user-visible changes. + +## 9. Verify + +```sh +cargo search seaography --limit 1 +``` + +Confirm crates.io shows the new versions and that docs.rs builds have started. Spot +check that `seaography-cli` generates a project whose `Cargo.toml` pins the intended +SeaORM and Seaography versions. + +## Notes + +- While SeaORM is in RC, the dependency **must** keep a pre-release form + (`~2.0.0-rc.N`); a plain `"2.0"` requirement will not resolve to an rc. Switch to + a stable requirement only once SeaORM `2.0.0` is published. +- Prefer raising the floor (`~2.0.0-rc.N`) over the bare `~2.0.0-rc` so a published + Seaography release cannot resolve an older, incompatible SeaORM rc. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index efef2656..4f4e2152 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-cli" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] @@ -15,5 +15,5 @@ categories = ["database"] [dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } clap = { version = "4.3.19", features = ["env", "derive"] } -seaography-generator = { version = "~2.0.0-rc.8", path = "../generator" } +seaography-generator = { version = "~2.0.0-rc.9", path = "../generator" } url = "2.4.0" \ No newline at end of file diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index 692901b9..61df79d9 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-mysql-example" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" [dependencies] actix-web = { version = "4.11" } @@ -12,12 +12,12 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.sea-orm] -version = "~2.0.0-rc" +version = "~2.0.0-rc.40" features = ["sqlx-mysql", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.8" # seaography version +version = "~2.0.0-rc.9" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index 88ee9473..3320b066 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" [dependencies] axum = { version = "0.8" } @@ -12,12 +12,12 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.sea-orm] -version = "~2.0.0-rc" +version = "~2.0.0-rc.40" features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.8" # seaography version +version = "~2.0.0-rc.9" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono", "with-json", "with-postgres-array", "schema-meta"] [dev-dependencies] diff --git a/examples/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index 16addb02..0b176ffa 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-draw" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" edition = "2024" [dependencies] @@ -15,7 +15,7 @@ reqwest = { version = "0.12", features = [ "rustls-tls", "multipart", ], default-features = false } -sea-orm = { version = "~2.0.0-rc", features = [ +sea-orm = { version = "~2.0.0-rc.40", features = [ "sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", @@ -30,7 +30,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.8" # seaography version +version = "~2.0.0-rc.9" # seaography version features = [ "macros", "field-pluralize", diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index db9635a9..d9d9e915 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" [dependencies] poem = { version = "3.0" } @@ -12,12 +12,12 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.sea-orm] -version = "~2.0.0-rc" +version = "~2.0.0-rc.40" features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~2.0.0-rc.8" # seaography version +version = "~2.0.0-rc.9" # seaography version features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] diff --git a/generator/Cargo.toml b/generator/Cargo.toml index fecd271c..acfed5e5 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-generator" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index 5cb5a020..bd6666da 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -12,7 +12,7 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.sea-orm] -version = "~2.0.0-rc" +version = "~2.0.0-rc.40" features = ["", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] diff --git a/generator/src/templates/axum_cargo.toml b/generator/src/templates/axum_cargo.toml index 931bfd03..0086b577 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -12,7 +12,7 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.sea-orm] -version = "~2.0.0-rc" +version = "~2.0.0-rc.40" features = ["", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] diff --git a/generator/src/templates/poem_cargo.toml b/generator/src/templates/poem_cargo.toml index 4c776fce..5e2b6553 100644 --- a/generator/src/templates/poem_cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -12,7 +12,7 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } [dependencies.sea-orm] -version = "~2.0.0-rc" +version = "~2.0.0-rc.40" features = ["", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index cd9c593b..59c0c56f 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seaography-macros" -version = "2.0.0-rc.8" +version = "2.0.0-rc.9" edition = "2024" rust-version = "1.85" authors = ["Chris Tsang "]