Expand SeaORM entity generator with Seaography related data (#1599)

* Add DeriveRelatedEntity macro

* Add generation for related enum and seaography

* Add seaography cli param

* update codegen tests

* Fix DeriveRelatedEntity macro doc and includes

* Fix all RelatedEntity variants for RelationBuilder

* Add tests for code

* Cargo format

* Fix clippy code

* Fix format

* Fix unit tests

* Fix unit tests

* Provide default for seaography::RelationBuilder

* Update changelog

* Update tests

* Modify code to match feedback
* Bring old Related Impl trait generation
* Modify DeriveRelatedEntity to gen impl seaography::RelationBuilder
* Generate RelatedEntity enum when seaography flag is enabled

* Update documentation

* Update Changelog

* Fix format errors

* Fix code generation
* relations with suffix are definition based
* Rev => Reverse easier to read
* snake_case to cameCase for name generation

* Fix unit tests

* Update lib.rs

* derive `seaography::RelationBuilder` only when `seaography` feature is enabled

* Try constructing async-graphql root for "related entity" and "entity" without relation

* Update demo

* CHANGELOG

* Update Cargo.toml

Co-authored-by: Chris Tsang <chris.2y3@outlook.com>

* Revert "Update Cargo.toml"

This reverts commit 6b1669836a4fb5040bfb08999f0cf640c74dc64d.

---------

Co-authored-by: Billy Chan <ccw.billy.123@gmail.com>
Co-authored-by: Chris Tsang <chris.2y3@outlook.com>
This commit is contained in:
Panagiotis Karatakis 2023-05-19 17:14:46 +03:00 committed by GitHub
parent fd6c303740
commit 3300336b1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1060 additions and 4 deletions

View File

@ -185,6 +185,33 @@ assert_eq!(
```
* [sea-orm-cli] Added support for generating migration of space separated name, for example executing `sea-orm-cli migrate generate "create accounts table"` command will create `m20230503_000000_create_accounts_table.rs` for you https://github.com/SeaQL/sea-orm/pull/1570
* Add `seaography` flag to `sea-orm`, `sea-orm-orm-macros` and `sea-orm-cli` https://github.com/SeaQL/sea-orm/pull/1599
* Add generation of `seaography` related information to `sea-orm-codegen` https://github.com/SeaQL/sea-orm/pull/1599
The following information is added in entities files by `sea-orm-cli` when flag `seaography` is `true`
```rust
/// ... Entity File ...
#[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()")]
SelfRefRev,
}
```
* Add `DeriveEntityRelated` macro https://github.com/SeaQL/sea-orm/pull/1599
The DeriveRelatedEntity derive macro will implement `seaography::RelationBuilder` for `RelatedEntity` enumeration when the `seaography` feature is enabled
### Enhancements
* Added `Migration::name()` and `Migration::status()` getters for the name and status of `sea_orm_migration::Migration` https://github.com/SeaQL/sea-orm/pull/1519

View File

@ -122,3 +122,4 @@ runtime-tokio-rustls = [
"runtime-tokio",
]
tests-cfg = ["serde/derive"]
seaography = ["sea-orm-macros/seaography"]

2
issues/1599/Cargo.toml Normal file
View File

@ -0,0 +1,2 @@
[workspace]
members = ["entity", "graphql"]

View File

@ -0,0 +1,17 @@
[package]
name = "entity"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "entity"
path = "src/lib.rs"
[dependencies]
sea-orm = { path = "../../../" }
seaography = { path = "../../../../seaography", optional = true }
async-graphql = { version = "5", optional = true }
[features]
seaography = ["dep:seaography", "async-graphql", "sea-orm/seaography"]

View File

@ -0,0 +1,60 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "cake")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_name = "name", enum_name = "Name")]
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::fruit::Entity")]
Fruit,
#[sea_orm(
has_many = "super::fruit::Entity",
on_condition = r#"super::fruit::Column::Name.like("%tropical%")"#
)]
TropicalFruit,
#[sea_orm(
has_many = "super::fruit::Entity",
condition_type = "any",
on_condition = r#"super::fruit::Column::Name.like("%tropical%")"#
)]
OrTropicalFruit,
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Filling.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Cake.def().rev())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::fruit::Entity")]
Fruit,
#[sea_orm(entity = "super::filling::Entity")]
Filling,
#[sea_orm(entity = "super::fruit::Entity", def = "Relation::TropicalFruit.def()")]
TropicalFruit,
#[sea_orm(
entity = "super::fruit::Entity",
def = "Relation::OrTropicalFruit.def()"
)]
OrTropicalFruit,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,70 @@
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"cake_filling"
}
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub cake_id: i32,
pub filling_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
CakeId,
FillingId,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
CakeId,
FillingId,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = (i32, i32);
fn auto_increment() -> bool {
false
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Cake,
Filling,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::CakeId => ColumnType::Integer.def(),
Self::FillingId => ColumnType::Integer.def(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Cake => Entity::belongs_to(super::cake::Entity)
.from(Column::CakeId)
.to(super::cake::Column::Id)
.into(),
Self::Filling => Entity::belongs_to(super::filling::Entity)
.from(Column::FillingId)
.to(super::filling::Column::Id)
.into(),
}
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,80 @@
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
#[sea_orm(table_name = "filling")]
pub struct Entity;
#[derive(Clone, Debug, PartialEq, Eq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub id: i32,
pub name: String,
pub vendor_id: Option<i32>,
#[sea_orm(ignore)]
pub ignored_attr: i32,
}
// If your column names are not in snake-case, derive `DeriveCustomColumn` here.
#[derive(Copy, Clone, Debug, EnumIter, DeriveCustomColumn)]
pub enum Column {
Id,
Name,
VendorId,
}
// Then, customize each column names here.
impl IdenStatic for Column {
fn as_str(&self) -> &str {
match self {
// Override column names
Self::Id => "id",
// Leave all other columns using default snake-case values
_ => self.default_as_str(),
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
Self::VendorId => ColumnType::Integer.def().nullable(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl Related<super::cake::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Cake.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Filling.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,29 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "fruit")]
pub struct Model {
#[sea_orm(primary_key)]
#[cfg_attr(feature = "with-json", serde(skip_deserializing))]
pub id: i32,
pub name: String,
pub cake_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::cake::Entity",
from = "Column::CakeId",
to = "super::cake::Column::Id"
)]
Cake,
}
impl Related<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,4 @@
pub mod cake;
pub mod cake_filling;
pub mod filling;
pub mod fruit;

View File

@ -0,0 +1,20 @@
[package]
name = "graphql"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
poem = { version = "1.3.55" }
async-graphql-poem = { version = "5.0.6" }
async-graphql = { version = "5.0.6", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] }
async-trait = { version = "0.1.64" }
dotenv = "0.15.0"
tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
tracing = { version = "0.1.37" }
tracing-subscriber = { version = "0.3.16" }
lazy_static = { version = "1.4.0" }
sea-orm = { path = "../../../" }
entity = { path = "../entity", features = ["seaography"] }
seaography = { path = "../../../../seaography" }

View File

@ -0,0 +1,64 @@
use async_graphql::{
dataloader::DataLoader,
http::{playground_source, GraphQLPlaygroundConfig},
};
use async_graphql_poem::GraphQL;
use dotenv::dotenv;
use lazy_static::lazy_static;
use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server};
use sea_orm::{prelude::*, Database};
use std::env;
pub mod query_root;
pub struct OrmDataloader {
pub db: DatabaseConnection,
}
lazy_static! {
static ref URL: String = env::var("URL").unwrap_or("0.0.0.0:8000".into());
static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into());
static ref DATABASE_URL: String =
env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set");
static ref DEPTH_LIMIT: Option<usize> = env::var("DEPTH_LIMIT").map_or(None, |data| Some(
data.parse().expect("DEPTH_LIMIT is not a number")
));
static ref COMPLEXITY_LIMIT: Option<usize> = env::var("COMPLEXITY_LIMIT")
.map_or(None, |data| {
Some(data.parse().expect("COMPLEXITY_LIMIT is not a number"))
});
}
#[handler]
async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new(&ENDPOINT)))
}
#[tokio::main]
async fn main() {
dotenv().ok();
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_test_writer()
.init();
let database = Database::connect(&*DATABASE_URL)
.await
.expect("Fail to initialize database connection");
let orm_dataloader: DataLoader<OrmDataloader> = DataLoader::new(
OrmDataloader {
db: database.clone(),
},
tokio::spawn,
);
let schema =
query_root::schema(database, orm_dataloader, *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);
Server::new(TcpListener::bind(&*URL))
.run(app)
.await
.expect("Fail to start web server");
}

View File

@ -0,0 +1,34 @@
use crate::OrmDataloader;
use async_graphql::{dataloader::DataLoader, dynamic::*};
use entity::*;
use sea_orm::DatabaseConnection;
use seaography::{Builder, BuilderContext};
lazy_static::lazy_static! { static ref CONTEXT : BuilderContext = BuilderContext :: default () ; }
pub fn schema(
database: DatabaseConnection,
orm_dataloader: DataLoader<OrmDataloader>,
depth: Option<usize>,
complexity: Option<usize>,
) -> Result<Schema, SchemaError> {
let mut builder = Builder::new(&CONTEXT);
// Register entity including relations
seaography::register_entities!(builder, [cake]);
// Register entity only, no relations
seaography::register_entities_without_relation!(builder, [cake_filling, filling, fruit]);
let schema = builder.schema_builder();
let schema = if let Some(depth) = depth {
schema.limit_depth(depth)
} else {
schema
};
let schema = if let Some(complexity) = complexity {
schema.limit_complexity(complexity)
} else {
schema
};
schema.data(database).data(orm_dataloader).finish()
}

View File

@ -321,6 +321,14 @@ pub enum GenerateSubcommands {
help = r#"Add extra attributes to generated model struct, no need for `#[]` (comma separated), e.g. `--model-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
)]
model_extra_attributes: Vec<String>,
#[clap(
action,
long,
default_value = "false",
long_help = "Generate helper Enumerations that are used by Seaography."
)]
seaography: bool,
},
}

View File

@ -31,6 +31,7 @@ pub async fn run_generate_command(
lib,
model_extra_derives,
model_extra_attributes,
seaography,
} => {
if verbose {
let _ = tracing_subscriber::fmt()
@ -172,6 +173,7 @@ pub async fn run_generate_command(
serde_skip_hidden_column,
model_extra_derives,
model_extra_attributes,
seaography,
);
let output = EntityTransformer::transform(table_stmts)?.generate(&writer_context);

View File

@ -92,6 +92,26 @@ impl Entity {
.collect()
}
/// Used to generate the names for the `enum RelatedEntity` that is useful to the Seaography project
pub fn get_related_entity_enum_name(&self) -> Vec<Ident> {
// 1st step get conjunct relations data
let conjunct_related_names = self.get_conjunct_relations_to_upper_camel_case();
// 2nd step get reverse self relations data
let self_relations_reverse = self
.relations
.iter()
.filter(|rel| rel.self_referencing)
.map(|rel| format_ident!("{}Reverse", rel.get_enum_name()));
// 3rd step get normal relations data
self.get_relation_enum_name()
.into_iter()
.chain(self_relations_reverse)
.chain(conjunct_related_names.into_iter())
.collect()
}
pub fn get_relation_defs(&self) -> Vec<TokenStream> {
self.relations.iter().map(|rel| rel.get_def()).collect()
}
@ -100,6 +120,64 @@ impl Entity {
self.relations.iter().map(|rel| rel.get_attrs()).collect()
}
/// Used to generate the attributes for the `enum RelatedEntity` that is useful to the Seaography project
pub fn get_related_entity_attrs(&self) -> Vec<TokenStream> {
// 1st step get conjunct relations data
let conjunct_related_attrs = self.conjunct_relations.iter().map(|conj| {
let entity = format!("super::{}::Entity", conj.get_to_snake_case());
quote! {
#[sea_orm(
entity = #entity
)]
}
});
// helper function that generates attributes for `Relation` data
let produce_relation_attrs = |rel: &Relation, reverse: bool| {
let entity = match rel.get_module_name() {
Some(module_name) => format!("super::{}::Entity", module_name),
None => String::from("Entity"),
};
if rel.self_referencing || !rel.impl_related || rel.num_suffix > 0 {
let def = if reverse {
format!("Relation::{}.def().rev()", rel.get_enum_name())
} else {
format!("Relation::{}.def()", rel.get_enum_name())
};
quote! {
#[sea_orm(
entity = #entity,
def = #def
)]
}
} else {
quote! {
#[sea_orm(
entity = #entity
)]
}
}
};
// 2nd step get reverse self relations data
let self_relations_reverse_attrs = self
.relations
.iter()
.filter(|rel| rel.self_referencing)
.map(|rel| produce_relation_attrs(rel, true));
// 3rd step get normal relations data
self.relations
.iter()
.map(|rel| produce_relation_attrs(rel, false))
.chain(self_relations_reverse_attrs)
.chain(conjunct_related_attrs)
.collect()
}
pub fn get_primary_key_auto_increment(&self) -> Ident {
let auto_increment = self.columns.iter().any(|col| col.auto_increment);
format_ident!("{}", auto_increment)

View File

@ -385,6 +385,7 @@ mod tests {
false,
&Default::default(),
&Default::default(),
false,
)
.into_iter()
.skip(1)

View File

@ -47,6 +47,7 @@ pub struct EntityWriterContext {
pub(crate) serde_skip_deserializing_primary_key: bool,
pub(crate) model_extra_derives: TokenStream,
pub(crate) model_extra_attributes: TokenStream,
pub(crate) seaography: bool,
}
impl WithSerde {
@ -142,6 +143,7 @@ impl EntityWriterContext {
serde_skip_hidden_column: bool,
model_extra_derives: Vec<String>,
model_extra_attributes: Vec<String>,
seaography: bool,
) -> Self {
Self {
expanded_format,
@ -154,6 +156,7 @@ impl EntityWriterContext {
serde_skip_hidden_column,
model_extra_derives: bonus_derive(model_extra_derives),
model_extra_attributes: bonus_attributes(model_extra_attributes),
seaography,
}
}
}
@ -209,6 +212,7 @@ impl EntityWriter {
serde_skip_hidden_column,
&context.model_extra_derives,
&context.model_extra_attributes,
context.seaography,
)
} else {
Self::gen_compact_code_blocks(
@ -220,6 +224,7 @@ impl EntityWriter {
serde_skip_hidden_column,
&context.model_extra_derives,
&context.model_extra_attributes,
context.seaography,
)
};
Self::write(&mut lines, code_blocks);
@ -323,6 +328,7 @@ impl EntityWriter {
serde_skip_hidden_column: bool,
model_extra_derives: &TokenStream,
model_extra_attributes: &TokenStream,
seaography: bool,
) -> Vec<TokenStream> {
let mut imports = Self::gen_import(with_serde);
imports.extend(Self::gen_import_active_enum(entity));
@ -349,6 +355,9 @@ impl EntityWriter {
code_blocks.extend(Self::gen_impl_related(entity));
code_blocks.extend(Self::gen_impl_conjunct_related(entity));
code_blocks.extend([Self::gen_impl_active_model_behavior()]);
if seaography {
code_blocks.extend([Self::gen_related_entity(entity)]);
}
code_blocks
}
@ -362,6 +371,7 @@ impl EntityWriter {
serde_skip_hidden_column: bool,
model_extra_derives: &TokenStream,
model_extra_attributes: &TokenStream,
seaography: bool,
) -> Vec<TokenStream> {
let mut imports = Self::gen_import(with_serde);
imports.extend(Self::gen_import_active_enum(entity));
@ -382,6 +392,9 @@ impl EntityWriter {
code_blocks.extend(Self::gen_impl_related(entity));
code_blocks.extend(Self::gen_impl_conjunct_related(entity));
code_blocks.extend([Self::gen_impl_active_model_behavior()]);
if seaography {
code_blocks.extend([Self::gen_related_entity(entity)]);
}
code_blocks
}
@ -608,6 +621,22 @@ impl EntityWriter {
.collect()
}
/// Used to generate `enum RelatedEntity` that is useful to the Seaography project
pub fn gen_related_entity(entity: &Entity) -> TokenStream {
let related_enum_name = entity.get_related_entity_enum_name();
let related_attrs = entity.get_related_entity_attrs();
quote! {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#(
#related_attrs
#related_enum_name
),*
}
}
}
pub fn gen_impl_conjunct_related(entity: &Entity) -> Vec<TokenStream> {
let table_name_camel_case = entity.get_table_name_camel_case_ident();
let via_snake_case = entity.get_conjunct_relations_via_snake_case();
@ -1371,6 +1400,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false
)
.into_iter()
.skip(1)
@ -1391,6 +1421,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)
@ -1411,6 +1442,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)
@ -1467,6 +1499,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)
@ -1487,6 +1520,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)
@ -1507,6 +1541,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)
@ -1539,6 +1574,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1554,6 +1590,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1569,6 +1606,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1582,6 +1620,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
@ -1597,6 +1636,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1612,6 +1652,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1627,6 +1668,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1640,6 +1682,104 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
Ok(())
}
#[test]
fn test_gen_with_seaography() -> io::Result<()> {
let cake_entity = Entity {
table_name: "cake".to_owned(),
columns: vec![
Column {
name: "id".to_owned(),
col_type: ColumnType::Integer,
auto_increment: true,
not_null: true,
unique: false,
},
Column {
name: "name".to_owned(),
col_type: ColumnType::Text,
auto_increment: false,
not_null: false,
unique: false,
},
Column {
name: "base_id".to_owned(),
col_type: ColumnType::Integer,
auto_increment: false,
not_null: false,
unique: false,
},
],
relations: vec![
Relation {
ref_table: "fruit".to_owned(),
columns: vec![],
ref_columns: vec![],
rel_type: RelationType::HasMany,
on_delete: None,
on_update: None,
self_referencing: false,
num_suffix: 0,
impl_related: true,
},
Relation {
ref_table: "cake".to_owned(),
columns: vec![],
ref_columns: vec![],
rel_type: RelationType::HasOne,
on_delete: None,
on_update: None,
self_referencing: true,
num_suffix: 0,
impl_related: true,
},
],
conjunct_relations: vec![ConjunctRelation {
via: "cake_filling".to_owned(),
to: "filling".to_owned(),
}],
primary_keys: vec![PrimaryKey {
name: "id".to_owned(),
}],
};
assert_eq!(cake_entity.get_table_name_snake_case(), "cake");
// Compact code blocks
assert_eq!(
comparable_file_string(include_str!("../../tests/with_seaography/cake.rs"))?,
generated_to_string(EntityWriter::gen_compact_code_blocks(
&cake_entity,
&WithSerde::None,
&DateTimeCrate::Chrono,
&None,
false,
false,
&TokenStream::new(),
&TokenStream::new(),
true,
))
);
// Expanded code blocks
assert_eq!(
comparable_file_string(include_str!("../../tests/with_seaography/cake_expanded.rs"))?,
generated_to_string(EntityWriter::gen_expanded_code_blocks(
&cake_entity,
&WithSerde::None,
&DateTimeCrate::Chrono,
&None,
false,
false,
&TokenStream::new(),
&TokenStream::new(),
true,
))
);
@ -1666,6 +1806,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1679,6 +1820,7 @@ mod tests {
false,
&bonus_derive(["ts_rs::TS"]),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1694,6 +1836,7 @@ mod tests {
false,
&bonus_derive(["ts_rs::TS", "utoipa::ToSchema"]),
&TokenStream::new(),
false,
))
);
@ -1711,6 +1854,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1726,6 +1870,7 @@ mod tests {
false,
&bonus_derive(["ts_rs::TS"]),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1741,6 +1886,7 @@ mod tests {
false,
&bonus_derive(["ts_rs::TS", "utoipa::ToSchema"]),
&TokenStream::new(),
false,
))
);
@ -1785,6 +1931,7 @@ mod tests {
bool,
&TokenStream,
&TokenStream,
bool,
) -> Vec<TokenStream>,
>,
) -> io::Result<()> {
@ -1815,6 +1962,7 @@ mod tests {
serde_skip_hidden_column,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.fold(TokenStream::new(), |mut acc, tok| {
@ -1846,6 +1994,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1861,6 +2010,7 @@ mod tests {
false,
&TokenStream::new(),
&bonus_attributes([r#"serde(rename_all = "camelCase")"#]),
false,
))
);
assert_eq!(
@ -1876,6 +2026,7 @@ mod tests {
false,
&TokenStream::new(),
&bonus_attributes([r#"serde(rename_all = "camelCase")"#, "ts(export)"]),
false,
))
);
@ -1893,6 +2044,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
))
);
assert_eq!(
@ -1908,6 +2060,7 @@ mod tests {
false,
&TokenStream::new(),
&bonus_attributes([r#"serde(rename_all = "camelCase")"#]),
false,
))
);
assert_eq!(
@ -1923,6 +2076,7 @@ mod tests {
false,
&TokenStream::new(),
&bonus_attributes([r#"serde(rename_all = "camelCase")"#, "ts(export)"]),
false,
))
);
@ -2015,6 +2169,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)
@ -2035,6 +2190,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)
@ -2055,6 +2211,7 @@ mod tests {
false,
&TokenStream::new(),
&TokenStream::new(),
false,
)
.into_iter()
.skip(1)

View File

@ -0,0 +1,50 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude:: * ;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "cake")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_type = "Text", nullable)]
pub name: Option<String> ,
pub base_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::fruit::Entity")]
Fruit,
#[sea_orm(has_one = "Entity")]
SelfRef ,
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Filling.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Cake.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::fruit::Entity")]
Fruit,
#[sea_orm(entity = "Entity", def = "Relation::SelfRef.def()")]
SelfRef,
#[sea_orm(entity = "Entity", def = "Relation::SelfRef.def().rev()")]
SelfRefReverse,
#[sea_orm(entity = "super::filling::Entity")]
Filling
}

View File

@ -0,0 +1,94 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude:: * ;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"cake"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)]
pub struct Model {
pub id: i32,
pub name: Option<String> ,
pub base_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Name,
BaseId,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Fruit,
SelfRef ,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::Text.def().null(),
Self::BaseId => ColumnType::Integer.def().null(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Fruit => Entity::has_many(super::fruit::Entity).into(),
Self::SelfRef => Entity::has_one(Entity).into(),
}
}
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Filling.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Cake.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::fruit::Entity")]
Fruit,
#[sea_orm(entity = "Entity", def = "Relation::SelfRef.def()")]
SelfRef,
#[sea_orm(entity = "Entity", def = "Relation::SelfRef.def().rev()")]
SelfRefReverse,
#[sea_orm(entity = "super::filling::Entity")]
Filling
}

View File

@ -34,3 +34,4 @@ default = ["derive"]
postgres-array = []
derive = ["bae"]
strum = []
seaography = []

View File

@ -33,3 +33,29 @@ pub mod field_attr {
pub condition_type: Option<syn::Lit>,
}
}
pub mod related_attr {
use bae::FromAttributes;
/// Operations for RelatedEntity enumeration
#[derive(Default, FromAttributes)]
pub struct SeaOrm {
///
/// Allows to modify target entity
///
/// Required on enumeration variants
///
/// If used on enumeration attributes
/// it allows to specify different
/// Entity ident
pub entity: Option<syn::Lit>,
///
/// Allows to specify RelationDef
///
/// Optional
///
/// If not supplied the generated code
/// will utilize `impl Related` trait
pub def: Option<syn::Lit>,
}
}

View File

@ -11,6 +11,7 @@ mod migration;
mod model;
mod partial_model;
mod primary_key;
mod related_entity;
mod relation;
mod try_getable_from_json;
mod util;
@ -27,5 +28,6 @@ pub use migration::*;
pub use model::*;
pub use partial_model::*;
pub use primary_key::*;
pub use related_entity::*;
pub use relation::*;
pub use try_getable_from_json::*;

View File

@ -0,0 +1,125 @@
use heck::ToLowerCamelCase;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use crate::derives::attributes::related_attr;
enum Error {
InputNotEnum,
InvalidEntityPath,
Syn(syn::Error),
}
struct DeriveRelatedEntity {
entity_ident: TokenStream,
ident: syn::Ident,
variants: syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
}
impl DeriveRelatedEntity {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let sea_attr = related_attr::SeaOrm::try_from_attributes(&input.attrs)
.map_err(Error::Syn)?
.unwrap_or_default();
let ident = input.ident;
let entity_ident = match sea_attr.entity.as_ref().map(Self::parse_lit_string) {
Some(entity_ident) => entity_ident.map_err(|_| Error::InvalidEntityPath)?,
None => quote! { Entity },
};
let variants = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
Ok(DeriveRelatedEntity {
entity_ident,
ident,
variants,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let ident = &self.ident;
let entity_ident = &self.entity_ident;
let variant_implementations: Vec<TokenStream> = self
.variants
.iter()
.map(|variant| {
let attr = related_attr::SeaOrm::from_attributes(&variant.attrs)?;
let enum_name = &variant.ident;
let target_entity = attr
.entity
.as_ref()
.map(Self::parse_lit_string)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'entity'")
})??;
let def = match attr.def {
Some(def) => Some(Self::parse_lit_string(&def).map_err(|_| {
syn::Error::new_spanned(variant, "Missing value for 'def'")
})?),
None => None,
};
let name = enum_name.to_string().to_lower_camel_case();
if let Some(def) = def {
Result::<_, syn::Error>::Ok(quote! {
Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def)
})
} else {
Result::<_, syn::Error>::Ok(quote! {
Self::#enum_name => via_builder.get_relation::<#entity_ident, #target_entity>(#name)
})
}
})
.collect::<Result<Vec<_>, _>>()?;
Ok(quote! {
impl seaography::RelationBuilder for #ident {
fn get_relation(&self, context: & 'static seaography::BuilderContext) -> async_graphql::dynamic::Field {
let builder = seaography::EntityObjectRelationBuilder { context };
let via_builder = seaography::EntityObjectViaRelationBuilder { context };
match self {
#(#variant_implementations,)*
_ => panic!("No relations for this entity"),
}
}
}
})
}
fn parse_lit_string(lit: &syn::Lit) -> syn::Result<TokenStream> {
match lit {
syn::Lit::Str(lit_str) => lit_str
.value()
.parse()
.map_err(|_| syn::Error::new_spanned(lit, "attribute not valid")),
_ => Err(syn::Error::new_spanned(lit, "attribute must be a string")),
}
}
}
/// Method to derive a Related enumeration
pub fn expand_derive_related_entity(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match DeriveRelatedEntity::new(input) {
Ok(model) => model.expand(),
Err(Error::InputNotEnum) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive DeriveRelation on enums");
}),
Err(Error::InvalidEntityPath) => Ok(quote_spanned! {
ident_span => compile_error!("invalid attribute value for 'entity'");
}),
Err(Error::Syn(err)) => Err(err),
}
}

View File

@ -639,6 +639,46 @@ pub fn derive_relation(input: TokenStream) -> TokenStream {
.into()
}
/// The DeriveRelatedEntity derive macro will implement seaography::RelationBuilder for RelatedEntity enumeration.
///
/// ### Usage
///
/// ```ignore
/// use sea_orm::entity::prelude::*;
///
/// // ...
/// // Model, Relation enum, etc.
/// // ...
///
/// #[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()")]
/// SelfRefRev,
/// }
/// ```
#[cfg(feature = "derive")]
#[proc_macro_derive(DeriveRelatedEntity, attributes(sea_orm))]
pub fn derive_related_entity(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
if cfg!(feature = "seaography") {
derives::expand_derive_related_entity(input)
.unwrap_or_else(Error::into_compile_error)
.into()
} else {
TokenStream::new()
}
}
/// The DeriveMigrationName derive macro will implement `sea_orm_migration::MigrationName` for a migration.
///
/// ### Usage

View File

@ -12,7 +12,7 @@ pub use crate::{
pub use crate::{
DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn,
DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel,
DerivePrimaryKey, DeriveRelation, FromJsonQueryResult,
DerivePrimaryKey, DeriveRelatedEntity, DeriveRelation, FromJsonQueryResult,
};
pub use async_trait;

View File

@ -350,8 +350,8 @@ pub use schema::*;
pub use sea_orm_macros::{
DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn,
DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel,
DeriveMigrationName, DeriveModel, DerivePartialModel, DerivePrimaryKey, DeriveRelation,
FromJsonQueryResult, FromQueryResult,
DeriveMigrationName, DeriveModel, DerivePartialModel, DerivePrimaryKey, DeriveRelatedEntity,
DeriveRelation, FromJsonQueryResult, FromQueryResult,
};
pub use sea_query;

View File

@ -0,0 +1,64 @@
use crate as sea_orm;
use crate::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "cake")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_name = "name", enum_name = "Name")]
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::fruit::Entity")]
Fruit,
#[sea_orm(
has_many = "super::fruit::Entity",
on_condition = r#"super::fruit::Column::Name.like("%tropical%")"#
)]
TropicalFruit,
#[sea_orm(
has_many = "super::fruit::Entity",
condition_type = "any",
on_condition = r#"super::fruit::Column::Name.like("%tropical%")"#
)]
OrTropicalFruit,
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Filling.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Cake.def().rev())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::fruit::Entity")]
Fruit,
#[sea_orm(entity = "super::filling::Entity")]
Filling,
#[sea_orm(
entity = "super::fruit::Entity",
def = "Relation::TropicalFruit.def()"
)]
TropicalFruit,
#[sea_orm(
entity = "super::fruit::Entity",
def = "Relation::OrTropicalFruit.def()"
)]
OrTropicalFruit,
}
impl ActiveModelBehavior for ActiveModel {}