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:
parent
fd6c303740
commit
3300336b1a
27
CHANGELOG.md
27
CHANGELOG.md
@ -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
|
||||
|
@ -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
2
issues/1599/Cargo.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[workspace]
|
||||
members = ["entity", "graphql"]
|
17
issues/1599/entity/Cargo.toml
Normal file
17
issues/1599/entity/Cargo.toml
Normal 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"]
|
60
issues/1599/entity/src/cake.rs
Normal file
60
issues/1599/entity/src/cake.rs
Normal 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 {}
|
70
issues/1599/entity/src/cake_filling.rs
Normal file
70
issues/1599/entity/src/cake_filling.rs
Normal 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 {}
|
80
issues/1599/entity/src/filling.rs
Normal file
80
issues/1599/entity/src/filling.rs
Normal 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 {}
|
29
issues/1599/entity/src/fruit.rs
Normal file
29
issues/1599/entity/src/fruit.rs
Normal 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 {}
|
4
issues/1599/entity/src/lib.rs
Normal file
4
issues/1599/entity/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod cake;
|
||||
pub mod cake_filling;
|
||||
pub mod filling;
|
||||
pub mod fruit;
|
20
issues/1599/graphql/Cargo.toml
Normal file
20
issues/1599/graphql/Cargo.toml
Normal 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" }
|
64
issues/1599/graphql/src/main.rs
Normal file
64
issues/1599/graphql/src/main.rs
Normal 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");
|
||||
}
|
34
issues/1599/graphql/src/query_root.rs
Normal file
34
issues/1599/graphql/src/query_root.rs
Normal 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()
|
||||
}
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -385,6 +385,7 @@ mod tests {
|
||||
false,
|
||||
&Default::default(),
|
||||
&Default::default(),
|
||||
false,
|
||||
)
|
||||
.into_iter()
|
||||
.skip(1)
|
||||
|
@ -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)
|
||||
|
50
sea-orm-codegen/tests/with_seaography/cake.rs
Normal file
50
sea-orm-codegen/tests/with_seaography/cake.rs
Normal 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
|
||||
}
|
94
sea-orm-codegen/tests/with_seaography/cake_expanded.rs
Normal file
94
sea-orm-codegen/tests/with_seaography/cake_expanded.rs
Normal 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
|
||||
}
|
@ -34,3 +34,4 @@ default = ["derive"]
|
||||
postgres-array = []
|
||||
derive = ["bae"]
|
||||
strum = []
|
||||
seaography = []
|
||||
|
@ -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>,
|
||||
}
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
125
sea-orm-macros/src/derives/related_entity.rs
Normal file
125
sea-orm-macros/src/derives/related_entity.rs
Normal 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),
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
64
src/tests_cfg/cake_seaography.rs
Normal file
64
src/tests_cfg/cake_seaography.rs
Normal 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 {}
|
Loading…
x
Reference in New Issue
Block a user