diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 78d72756..e36b20cd 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,16 +51,19 @@ 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: crate: sea-orm-cli - version: 1.0.0 + version: 2.0.0-rc.18 - name: Additional Integration tests working-directory: ./examples/sqlite 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 @@ -78,13 +80,15 @@ jobs: with: command: run args: > - --package seaography-cli -- ./examples/sqlite - ./examples/sqlite/src/entities sqlite://sakila.db - seaography-sqlite-example -f actix + --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: >- - 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 @@ -112,11 +116,13 @@ 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: crate: sea-orm-cli - version: 1.0.0 + version: 2.0.0-rc.13 - name: Remove generated folder run: rm -rf ./examples/mysql/src - name: Create DB @@ -140,15 +146,15 @@ jobs: with: command: run args: > - --package seaography-cli -- ./examples/mysql - ./examples/mysql/src/entities mysql://sea:sea@127.0.0.1/sakila - seaography-mysql-example -f axum + --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: >- - 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' + 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 @@ -173,11 +179,13 @@ 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: crate: sea-orm-cli - version: 1.0.0 + version: 2.0.0-rc.13 - name: Create DB run: >- psql -q postgres://sea:sea@localhost/postgres -c 'CREATE DATABASE @@ -195,7 +203,8 @@ jobs: working-directory: ./examples/postgres run: | cargo test --test pg_query_tests - rm tests/pg_query_tests.rs + 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 - name: Generate entities @@ -207,36 +216,23 @@ jobs: with: command: run args: > - --package seaography-cli -- ./examples/postgres - ./examples/postgres/src/entities - postgres://sea:sea@127.0.0.1/sakila?currentSchema=public - seaography-postgres-example -f poem + --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: >- - 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' + 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 - 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: @@ -256,6 +252,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/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 diff --git a/Cargo.toml b/Cargo.toml index 49bbc20a..7bc64c63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,14 +3,14 @@ members = [".", "cli", "generator", "macros"] [package] name = "seaography" -version = "1.1.4" +version = "2.0.0-rc.8" edition = "2021" rust-version = "1.70" 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" @@ -18,22 +18,29 @@ 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 = "1.1.15", default-features = false, features = ["seaography", "with-json"] } -seaography-macros = { version = "0.1.0", path = "macros", optional = true } +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 } itertools = { version = "0.12.0" } 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"] with-time = ["sea-orm/with-time", "async-graphql/time", "time"] @@ -43,11 +50,11 @@ 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"] 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" } +# sea-orm = { git = "https://github.com/SeaQL/sea-orm", branch = "master" } diff --git a/README.md b/README.md index 2fb882a1..fc32bc59 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ Seaography logo -

- 🧭 A dynamic GraphQL framework for SeaORM -

+

🧭 A GraphQL framework for Rust

+

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) @@ -14,16 +13,19 @@ # 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. +## Introduction -Seaography enables you to focus on your application logic instead of boilerplate. -With Seaography, you can: +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. -+ 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 +With Seaography you can focus on application logic instead of boilerplate. It enables you to: + ++ 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 ++ Use RBAC, guards, and lifecycle hooks to implement authorization and custom business logic ## Supported technologies @@ -36,35 +38,45 @@ 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) + +### SeaORM Version Compatibility -+ [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) +| 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 | -|----------------------------------------------------------|-------------------------------------------------------| -| [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! ### 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 @@ -75,7 +87,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 ``` @@ -87,7 +99,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 ``` @@ -98,7 +110,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 ``` @@ -234,7 +246,7 @@ Find all inactive customers, include their address, and their payments with amou } ``` -### Filter using enumeration +### Filter using MySQL / Postgres enum ```graphql { film( @@ -243,6 +255,7 @@ Find all inactive customers, include their address, and their payments with amou ) { nodes { filmId + title rating } } diff --git a/build-tools/bump-version.sh b/build-tools/bump.sh similarity index 79% rename from build-tools/bump-version.sh rename to build-tools/bump.sh index 323c1f80..73d5e499 100644 --- a/build-tools/bump-version.sh +++ b/build-tools/bump.sh @@ -4,22 +4,23 @@ 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-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" -sleep 1 # Bump examples' dependency version cd examples diff --git a/build-tools/cargo-publish.sh b/build-tools/publish.sh similarity index 75% rename from build-tools/cargo-publish.sh rename to build-tools/publish.sh index cbb62000..db3a5e9d 100644 --- a/build-tools/cargo-publish.sh +++ b/build-tools/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/cli/Cargo.toml b/cli/Cargo.toml index 9415bc52..efef2656 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "seaography-cli" -version = "1.1.4" +version = "2.0.0-rc.8" edition = "2021" rust-version = "1.70" authors = ["Panagiotis Karatakis "] -description = "🧭 A dynamic 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" ] } -clap = { version = "4.3.19", features = ["derive"] } -seaography-generator = { version = "~1.1.4", path = "../generator" } +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" } url = "2.4.0" \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index a89af24f..8b14e460 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,13 +4,24 @@ use seaography_generator::write_project; #[derive(clap::Parser)] #[clap(author, version, about, long_about = None)] pub struct Args { - /// Project destination folder - pub destination: String, - - /// SeaORM entities folder + #[arg( + short = 'o', + long, + default_value = "./", + help = "Project output directory" + )] + pub output_dir: String, + + #[arg(short = 'e', long, help = "Entities directory")] pub entities: String, - /// Database URL to write in .env + #[arg( + short = 'u', + long, + env = "DATABASE_URL", + help = "Database URL", + hide_env_values = true + )] pub database_url: String, /// Crate name for generated project @@ -83,11 +94,11 @@ pub fn parse_database_url(database_url: &str) -> Result, + #[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/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/mysql/src/main.rs b/examples/mysql/src/main.rs index 878fc333..8e66ab60 100644 --- a/examples/mysql/src/main.rs +++ b/examples/mysql/src/main.rs @@ -1,17 +1,15 @@ -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 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 = @@ -25,12 +23,18 @@ lazy_static::lazy_static! { }); } -async fn graphiql() -> impl IntoResponse { - response::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)))) } -#[tokio::main] -async fn main() { +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(); 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::Get()) + .to(graphql_playground), + ) + .service(web::resource("/").guard(guard::Post()).to(graphql_handler)) + }) + .bind("127.0.0.1:8000")? + .run() + .await } 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) 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/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; diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index b9fb360d..88ee9473 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -1,24 +1,28 @@ [package] edition = "2021" name = "seaography-postgres-example" -version = "1.1.4" +version = "2.0.0-rc.8" [dependencies] -poem = { version = "3.0" } -async-graphql-poem = { version = "7.0" } +axum = { version = "0.8" } +async-graphql-axum = { version = "7.0" } dotenv = "0.15.0" -sea-orm = { version = "~1.1.14", 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.sea-orm] +version = "~2.0.0-rc" +features = ["sqlx-postgres", "runtime-tokio-native-tls", "seaography"] + [dependencies.seaography] path = "../../" -version = "~1.1.4" # seaography version -features = ["with-decimal", "with-chrono", "with-json", "with-postgres-array"] +version = "~2.0.0-rc.8" # seaography version +features = ["graphql-playground", "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] -members = [] \ No newline at end of file +members = [] 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 598b22bc..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,20 @@ 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 cb574fbe..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; @@ -16,3 +18,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/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..e09d25a8 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)] @@ -5,11 +7,14 @@ 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, - pub customer_id: i16, + #[sea_orm(unique_key = "unique")] + 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 85faa065..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)] @@ -5,12 +7,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..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,14 +9,15 @@ 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, pub last_update: DateTime, - #[sea_orm(column_type = "Binary(255)", nullable)] + #[sea_orm(column_type = "VarBinary(StringLen::None)", nullable)] pub picture: Option>, } @@ -32,6 +35,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 +87,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..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)] @@ -5,8 +7,9 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub store_id: i32, - pub manager_staff_id: i16, - pub address_id: i16, + #[sea_orm(unique)] + pub manager_staff_id: i32, + pub address_id: i32, pub last_update: DateTime, } diff --git a/examples/postgres/src/main.rs b/examples/postgres/src/main.rs index d0f34368..066df05e 100644 --- a/examples/postgres/src/main.rs +++ b/examples/postgres/src/main.rs @@ -1,12 +1,21 @@ -use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; -use async_graphql_poem::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 poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; 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 = @@ -20,9 +29,13 @@ lazy_static::lazy_static! { }); } -#[handler] async fn graphql_playground() -> impl IntoResponse { - Html(playground_source(GraphQLPlaygroundConfig::new(&ENDPOINT))) + 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] @@ -32,19 +45,17 @@ 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 = Route::new().at( - &*ENDPOINT, - get(graphql_playground).post(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); - 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..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, - [ - film_actor, - rental, - category, - staff, - country, - film, - actor, - language, - city, - inventory, - film_category, - customer, - store, - payment, - address, - ] - ); - builder.register_enumeration::(); + builder = register_entity_modules(builder); + builder = register_active_enums(builder); builder .set_depth_limit(depth) .set_complexity_limit(complexity) diff --git a/examples/postgres/tests/entity_metadata_tests.rs b/examples/postgres/tests/entity_metadata_tests.rs new file mode 100644 index 00000000..bd5df445 --- /dev/null +++ b/examples/postgres/tests/entity_metadata_tests.rs @@ -0,0 +1,265 @@ +use async_graphql::{dynamic::*, Response}; +use sea_orm::Database; +use seaography::async_graphql; + +pub async fn 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 = 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", + "PG", + "PG-13", + "R", + "NC-17" + ] + } + } + }, + { + "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 + } + }, + { + "name": "metadata", + "nullable": true, + "type_": { + "primitive": "json", + "array": null, + "enumeration": null + } + } + ], + "primary_key": [ + "film_id" + ] + } + } + "#, + ) +} 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 afa4590c..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(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/postgres/tests/mutation_tests.rs b/examples/postgres/tests/mutation_tests.rs index 475ee49e..68939030 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,23 +425,60 @@ 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() { let schema = schema().await; + restore_country(&schema).await; + assert_eq( schema .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 +522,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 +566,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 +609,46 @@ async fn test_update_mutation() { } "#, ); + + restore_country(&schema).await; +} + +async fn restore_country(schema: &Schema) { + 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 +659,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 +682,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 +699,13 @@ async fn test_delete_mutation() { { "languageCreateBatch": [ { - "languageId": 9 + "name": "9 " }, { - "languageId": 10 + "name": "10 " }, { - "languageId": 11 + "name": "11 " } ] } @@ -558,11 +717,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 +731,13 @@ async fn test_delete_mutation() { "language": { "nodes": [ { - "languageId": 9 + "name": "9 " }, { - "languageId": 10 + "name": "10 " }, { - "languageId": 11 + "name": "11 " } ] } @@ -591,14 +750,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 +767,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": [ + ] + } } "#, ); 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 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/sea-draw/Cargo.toml b/examples/sea-draw/Cargo.toml index b39956ec..16addb02 100644 --- a/examples/sea-draw/Cargo.toml +++ b/examples/sea-draw/Cargo.toml @@ -1,13 +1,12 @@ [package] name = "sea-draw" -version = "0.1.0" +version = "2.0.0-rc.8" 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"] } -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.8" # seaography version features = [ "macros", "field-pluralize", 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/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/sea-draw/src/schema.rs b/examples/sea-draw/src/schema.rs index e34183e1..2340d8d3 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() }, @@ -341,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/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" + // ); + // }) } } diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 0a514b01..db9635a9 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -1,29 +1,34 @@ [package] edition = "2021" name = "seaography-sqlite-example" -version = "1.1.4" +version = "2.0.0-rc.8" [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 = "~1.1.14", 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" } -serde = { version = "1", features = ["derive"] } -serde_json = { version = "1.0.103" } + +[dependencies.sea-orm] +version = "~2.0.0-rc" +features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] [dependencies.seaography] path = "../../" -version = "~1.1.4" # seaography version -features = ["macros", "with-decimal", "with-chrono"] +version = "~2.0.0-rc.8" # seaography version +features = ["graphql-playground", "with-decimal", "with-chrono"] + +[dev-dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0.103" } [workspace] members = [] [features] +rbac = ["seaography/rbac"] field-pluralize = ["seaography/field-pluralize"] [[test]] 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 e511f197..373d8cb5 100644 Binary files a/examples/sqlite/sakila.db and b/examples/sqlite/sakila.db differ diff --git a/examples/sqlite/src/entities/actor.rs b/examples/sqlite/src/entities/actor.rs index 3dc2de64..26cbb007 100644 --- a/examples/sqlite/src/entities/actor.rs +++ b/examples/sqlite/src/entities/actor.rs @@ -1,42 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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..3c758c8c 100644 --- a/examples/sqlite/src/entities/address.rs +++ b/examples/sqlite/src/entities/address.rs @@ -1,71 +1,34 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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..022a407c 100644 --- a/examples/sqlite/src/entities/category.rs +++ b/examples/sqlite/src/entities/category.rs @@ -1,41 +1,17 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "category")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] 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..bbdd2f8a 100644 --- a/examples/sqlite/src/entities/city.rs +++ b/examples/sqlite/src/entities/city.rs @@ -1,47 +1,26 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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..78061040 100644 --- a/examples/sqlite/src/entities/country.rs +++ b/examples/sqlite/src/entities/country.rs @@ -1,30 +1,17 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "country")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] 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..6c6f74f0 100644 --- a/examples/sqlite/src/entities/customer.rs +++ b/examples/sqlite/src/entities/customer.rs @@ -1,78 +1,41 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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 9a5e7832..c1d41a35 100644 --- a/examples/sqlite/src/entities/film.rs +++ b/examples/sqlite/src/entities/film.rs @@ -1,11 +1,15 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, pub release_year: Option, pub language_id: i16, @@ -19,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..ba665aa3 100644 --- a/examples/sqlite/src/entities/film_actor.rs +++ b/examples/sqlite/src/entities/film_actor.rs @@ -1,53 +1,32 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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..72c77353 100644 --- a/examples/sqlite/src/entities/film_category.rs +++ b/examples/sqlite/src/entities/film_category.rs @@ -1,53 +1,32 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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 00bbfab9..584b167c 100644 --- a/examples/sqlite/src/entities/film_text.rs +++ b/examples/sqlite/src/entities/film_text.rs @@ -1,18 +1,16 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "film_text")] 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, } -#[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..049e5a4c 100644 --- a/examples/sqlite/src/entities/inventory.rs +++ b/examples/sqlite/src/entities/inventory.rs @@ -1,63 +1,34 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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 650001e3..6cb20ea1 100644 --- a/examples/sqlite/src/entities/language.rs +++ b/examples/sqlite/src/entities/language.rs @@ -1,19 +1,15 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "language")] 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, } -#[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 8fbead53..ffc09e46 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.16 + pub mod prelude; pub mod actor; @@ -16,3 +18,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/payment.rs b/examples/sqlite/src/entities/payment.rs index 26c1aee1..24451ab2 100644 --- a/examples/sqlite/src/entities/payment.rs +++ b/examples/sqlite/src/entities/payment.rs @@ -1,73 +1,44 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, 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..883d6258 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.16 + 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..be95e3f0 100644 --- a/examples/sqlite/src/entities/rental.rs +++ b/examples/sqlite/src/entities/rental.rs @@ -1,82 +1,48 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, - pub inventory_id: i32, - pub customer_id: i32, + #[sea_orm(unique_key = "unique")] + pub inventory_id: i64, + #[sea_orm(unique_key = "unique")] + pub customer_id: i64, 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 bdf6faf3..44aef15a 100644 --- a/examples/sqlite/src/entities/staff.rs +++ b/examples/sqlite/src/entities/staff.rs @@ -1,94 +1,54 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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 = "Binary(255)", nullable)] + #[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, 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..e51d2f19 100644 --- a/examples/sqlite/src/entities/store.rs +++ b/examples/sqlite/src/entities/store.rs @@ -1,73 +1,36 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm::model] +#[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, -} - -#[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, -} diff --git a/examples/sqlite/src/main.rs b/examples/sqlite/src/main.rs index 52da0a8c..910681a5 100644 --- a/examples/sqlite/src/main.rs +++ b/examples/sqlite/src/main.rs @@ -1,15 +1,20 @@ -use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer, Result}; use async_graphql::{ - dynamic::*, + dynamic::Schema, http::{playground_source, GraphQLPlaygroundConfig}, }; -use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; +use async_graphql_poem::{GraphQLRequest, GraphQLResponse}; use dotenv::dotenv; +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 = @@ -23,18 +28,19 @@ 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)))) +#[handler] +async fn graphql_handler(schema: Data<&Schema>, req: GraphQLRequest) -> GraphQLResponse { + let req = req.0; + schema.execute(req).await.into() } -#[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 +52,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_handler).data(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.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, ..]); 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/custom_query_tests.rs b/examples/sqlite/tests/custom_query_tests.rs index 430b7ab3..541d39ce 100644 --- a/examples/sqlite/tests/custom_query_tests.rs +++ b/examples/sqlite/tests/custom_query_tests.rs @@ -267,9 +267,7 @@ async fn option_entity_object_relation_owner() { json!({ "staff_by_id": { "selfRefReverse": { - "nodes": [{ - "firstName": "Jon" - }] + "nodes": [{"firstName": "Jon"}, {"firstName": "Emily"}] } } }) 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/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/examples/sqlite/tests/hook_guard_tests.rs b/examples/sqlite/tests/hook_guard_tests.rs index 1b806ca7..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)] @@ -71,11 +77,10 @@ struct Log { #[derive(Default, Clone)] struct Permissions { - actors: BTreeSet, + actors: BTreeSet, staff: BTreeSet, } -#[derive(Clone)] struct MyHooks; impl Log { @@ -84,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() } } @@ -166,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) { @@ -263,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), ] ); } @@ -310,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), ] ); } @@ -379,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), @@ -473,6 +497,7 @@ async fn entity_object_relation_owner1() { let mut permissions = Permissions::default(); permissions.staff.insert(1); permissions.staff.insert(2); + permissions.staff.insert(3); let (schema, log) = schema(permissions).await; let query = entity_object_relation_owner_query(); let vars = Variables::from_json(json!({ "staffId": 1 })); @@ -484,9 +509,7 @@ async fn entity_object_relation_owner1() { "staff": { "nodes": [{ "selfRefReverse": { - "nodes": [{ - "firstName": "Jon" - }] + "nodes": [{"firstName": "Jon"}, {"firstName": "Emily"}] } }] } @@ -499,6 +522,7 @@ async fn entity_object_relation_owner1() { HookCall::entity_guard("Staff", OperationType::Read), HookCall::field_guard("Staff", Some(1), "selfRefReverse", OperationType::Read), HookCall::field_guard("Staff", Some(2), "firstName", OperationType::Read), + HookCall::field_guard("Staff", Some(3), "firstName", OperationType::Read), ] ); } @@ -642,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), ] ); } @@ -727,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), ] ); } @@ -807,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/examples/sqlite/tests/more_query_tests.rs b/examples/sqlite/tests/more_query_tests.rs index e874f002..9fc7ba2b 100644 --- a/examples/sqlite/tests/more_query_tests.rs +++ b/examples/sqlite/tests/more_query_tests.rs @@ -1,17 +1,12 @@ 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! { 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/mutation_tests.rs b/examples/sqlite/tests/mutation_tests.rs index 1cc9e370..d670e26e 100644 --- a/examples/sqlite/tests/mutation_tests.rs +++ b/examples/sqlite/tests/mutation_tests.rs @@ -1,14 +1,21 @@ 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; test_update_mutation().await; test_delete_mutation().await; + test_add_original_language_to_film().await; } async fn schema() -> Schema { @@ -26,6 +33,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( @@ -108,15 +125,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 @@ -125,101 +172,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( @@ -317,6 +395,16 @@ async fn test_create_batch_mutation() { } "#, ); + + schema + .execute( + r#" + mutation { + filmTextDelete(filter: { }) + } + "#, + ) + .await; } async fn test_update_mutation() { @@ -327,12 +415,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 } + } } "#, ) @@ -364,22 +452,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 } ] } @@ -392,13 +464,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 + } } "#, ) @@ -436,12 +508,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 } + } } "#, ) @@ -473,28 +545,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() { @@ -560,16 +652,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( @@ -600,4 +688,196 @@ 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( + 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" + } + } + ] + } + } + "#, + ); + + 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 + } + ] + } + } + "#, + ); } diff --git a/examples/sqlite/tests/plural_query_tests.rs b/examples/sqlite/tests/plural_query_tests.rs index 54b45f0a..f1742d6b 100644 --- a/examples/sqlite/tests/plural_query_tests.rs +++ b/examples/sqlite/tests/plural_query_tests.rs @@ -8,10 +8,9 @@ async fn schema() -> Schema { } fn assert_eq(a: Response, b: &str) { - assert_eq!( - a.data.into_json().unwrap(), - serde_json::from_str::(b).unwrap() - ) + let json = a.data.into_json().unwrap(); + println!("{}", serde_json::to_string(&json).unwrap()); + assert_eq!(json, serde_json::from_str::(b).unwrap()) } #[tokio::test] @@ -285,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" } } } @@ -362,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" } } } @@ -430,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" } } } @@ -470,34 +469,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" } + } ] - } + } } "#, ) @@ -514,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 @@ -604,7 +612,7 @@ async fn related_queries_filters() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "endCursor": "Int[3]:406" + "endCursor": "BigInt[3]:406" } } } @@ -716,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 @@ -834,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 d429cfaa..6bef62cd 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) { @@ -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" } } } @@ -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" } + } ] - } + } } "#, ) @@ -613,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 @@ -703,7 +712,7 @@ async fn related_queries_filters() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "endCursor": "Int[3]:406" + "endCursor": "BigInt[3]:406" } } } @@ -815,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 @@ -933,7 +942,7 @@ async fn related_queries_pagination() { "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, - "endCursor": "Int[3]:406" + "endCursor": "BigInt[3]:406" } } } @@ -1143,7 +1152,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 { @@ -1179,3 +1188,435 @@ async fn filter_is_null() { "#, ); } + +#[tokio::test] +async fn film_having_actor() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + film( + filters: { filmId: { ne: 2 } } + 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": 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" + } + ] + } + }, + { + "filmId": 144, + "title": "CHINATOWN GLADIATOR", + "actor": { + "nodes": [ + { + "firstName": "UMA", + "lastName": "WOOD" + }, + { + "firstName": "DAN", + "lastName": "TORN" + }, + { + "firstName": "BOB", + "lastName": "FAWCETT" + }, + { + "firstName": "JUDE", + "lastName": "CRUISE" + }, + { + "firstName": "JESSICA", + "lastName": "BAILEY" + }, + { + "firstName": "SEAN", + "lastName": "WILLIAMS" + }, + { + "firstName": "PENELOPE", + "lastName": "MONROE" + }, + { + "firstName": "GEOFFREY", + "lastName": "HESTON" + }, + { + "firstName": "JEFF", + "lastName": "SILVERSTONE" + }, + { + "firstName": "JAYNE", + "lastName": "SILVERSTONE" + } + ] + } + } + ] + } + } + "#, + ) +} + +#[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" + } + ] + } + } + "#, + ) +} + +#[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" + } + ] + } + } + "#, + ) +} + +#[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" + } + ] + } + } + ] + } + } + "#, + ) +} diff --git a/generator/Cargo.toml b/generator/Cargo.toml index 467b29c1..fecd271c 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "seaography-generator" -version = "1.1.4" +version = "2.0.0-rc.8" 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" @@ -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/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..74db2e1b 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 = @@ -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() diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index 8cf3b723..5cb5a020 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -4,17 +4,20 @@ 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" -sea-orm = { version = "~1.1.5", 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" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +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.rs b/generator/src/templates/axum.rs index f5a4c39f..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 = @@ -37,10 +38,15 @@ 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))) } + 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(graphiql).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 98491d21..931bfd03 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -4,17 +4,20 @@ 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 = "~1.1.5", 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" } +[dependencies.sea-orm] +version = "~2.0.0-rc" +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.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/templates/poem_cargo.toml b/generator/src/templates/poem_cargo.toml index f67cc49a..4c776fce 100644 --- a/generator/src/templates/poem_cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -7,14 +7,17 @@ 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"] } 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"] +features = ["graphql-playground", "with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } diff --git a/generator/src/writer.rs b/generator/src/writer.rs index 875558e7..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(); } @@ -105,16 +101,11 @@ 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, - [ - #(#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)?; diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 18f37d78..cd9c593b 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,7 +1,16 @@ [package] name = "seaography-macros" -version = "0.1.0" +version = "2.0.0-rc.8" 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 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; + /// The Builder is used to create the Schema for GraphQL /// /// You can populate it with the entities, enumerations of your choice @@ -52,7 +55,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, @@ -105,9 +108,13 @@ impl Builder { } } - /// used to register a new entity to the Builder context - pub fn register_entity(&mut self, relations: Vec) - where + /// Register a SeaORM Entity to the schema. + /// With all the edge, connection and filter objects. + pub fn register_entity( + &mut self, + relations: Vec, + related_entity_filter: &RelatedEntityFilter, + ) where T: EntityTrait, ::Model: Sync, { @@ -129,18 +136,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, @@ -159,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, @@ -191,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, @@ -206,7 +219,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 { @@ -269,7 +282,18 @@ impl Builder { self } - /// used to register a new enumeration to the builder context + 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 an SeaORM ActiveEnum to the schema pub fn register_enumeration(&mut self) where A: ActiveEnum, @@ -313,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, @@ -320,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, @@ -373,32 +399,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::CustomOutputType; + 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 +437,27 @@ impl Builder { TABLE_NAME, TypeRef::named_nn(TypeRef::STRING), )); - let query = query.field(field); + + self.queries.push(field); + } + + /// 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(); + + 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 @@ -537,10 +587,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] @@ -611,18 +665,30 @@ 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) => { - $builder.register_entity::<$module_path::Entity>( + seaography::register_entity!($builder, $module_path, mutation: true); + }; + ($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(), - ); + .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.register_entity_mutations::<$module_path::Entity, $module_path::ActiveModel>(); + $builder = + $builder.register_related_entity_filter::<$module_path::Entity>(related_entity_filter); + if $mutation { + $builder.register_entity_mutations::<$module_path::Entity, $module_path::ActiveModel>(); + } }; } @@ -680,6 +746,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/builder_context.rs b/src/builder_context.rs index f82f5d6f..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, @@ -55,7 +56,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/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 5f55be8c..f40e63ea 100644 --- a/src/builder_context/filter_types_map.rs +++ b/src/builder_context/filter_types_map.rs @@ -1,11 +1,11 @@ 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, - 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(); } @@ -291,7 +280,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())) } @@ -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 { @@ -661,14 +646,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/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/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/builder_context/types_map.rs b/src/builder_context/types_map.rs index 85fc26a2..70467756 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, }, } @@ -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, @@ -632,7 +634,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 +646,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 => { @@ -653,7 +655,7 @@ 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()?, @@ -667,11 +669,11 @@ 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}"), ) })?; - sea_orm::Value::ChronoDate(Some(Box::new(value))) + sea_orm::Value::ChronoDate(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoTime => { @@ -680,11 +682,11 @@ 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}"), ) })?; - sea_orm::Value::ChronoTime(Some(Box::new(value))) + sea_orm::Value::ChronoTime(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTime => { @@ -695,11 +697,11 @@ 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}"), ) })?; - sea_orm::Value::ChronoDateTime(Some(Box::new(value))) + sea_orm::Value::ChronoDateTime(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTimeUtc => { @@ -709,11 +711,11 @@ 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}"), ) })?; - sea_orm::Value::ChronoDateTimeUtc(Some(Box::new(value))) + sea_orm::Value::ChronoDateTimeUtc(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTimeLocal => { @@ -723,11 +725,11 @@ 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}"), ) })?; - sea_orm::Value::ChronoDateTimeLocal(Some(Box::new(value))) + sea_orm::Value::ChronoDateTimeLocal(Some(value)) } #[cfg(feature = "with-chrono")] ConvertedType::ChronoDateTimeWithTimeZone => { @@ -738,14 +740,11 @@ 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}",), ) })?; - sea_orm::Value::ChronoDateTimeWithTimeZone(Some(Box::new(value))) + sea_orm::Value::ChronoDateTimeWithTimeZone(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeDate => { @@ -756,11 +755,11 @@ 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}"), ) })?; - sea_orm::Value::TimeDate(Some(Box::new(value))) + sea_orm::Value::TimeDate(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeTime => { @@ -771,11 +770,11 @@ 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}"), ) })?; - sea_orm::Value::TimeTime(Some(Box::new(value))) + sea_orm::Value::TimeTime(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeDateTime => { @@ -786,11 +785,11 @@ 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}"), ) })?; - sea_orm::Value::TimeDateTime(Some(Box::new(value))) + sea_orm::Value::TimeDateTime(Some(value)) } #[cfg(feature = "with-time")] ConvertedType::TimeDateTimeWithTimeZone => { @@ -801,11 +800,11 @@ 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}"), ) })?; - sea_orm::Value::TimeDateTimeWithTimeZone(Some(Box::new(value))) + sea_orm::Value::TimeDateTimeWithTimeZone(Some(value)) } #[cfg(feature = "with-uuid")] ConvertedType::Uuid => { @@ -814,11 +813,11 @@ 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}"), ) })?; - sea_orm::Value::Uuid(Some(Box::new(value))) + sea_orm::Value::Uuid(Some(value)) } #[cfg(feature = "with-decimal")] ConvertedType::Decimal => { @@ -828,11 +827,11 @@ 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}"), ) })?; - sea_orm::Value::Decimal(Some(Box::new(value))) + sea_orm::Value::Decimal(Some(value)) } #[cfg(feature = "with-bigdecimal")] ConvertedType::BigDecimal => { @@ -842,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}"), ) })?; @@ -864,13 +863,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/custom/aux_types.rs b/src/custom/aux_types.rs index 1cbc97a6..2e1012ac 100644 --- a/src/custom/aux_types.rs +++ b/src/custom/aux_types.rs @@ -84,7 +84,7 @@ where value .ok_or_else(|| { let type_name = entity_object_builder.type_name::(); - 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..3837cc40 100644 --- a/src/custom/input.rs +++ b/src/custom/input.rs @@ -4,8 +4,26 @@ use async_graphql::{ Upload, }; #[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use seaography_macros::CustomInputType; +/// ``` +/// 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 trait CustomInputType: Sized { fn gql_input_type_ref(ctx: &'static BuilderContext) -> TypeRef; @@ -101,7 +119,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/custom/output.rs b/src/custom/output.rs index 9cd040ff..d1fcd5ea 100644 --- a/src/custom/output.rs +++ b/src/custom/output.rs @@ -4,8 +4,30 @@ 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; +/// +/// #[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 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 2643182b..99656cfc 100644 --- a/src/custom/types.rs +++ b/src/custom/types.rs @@ -6,12 +6,59 @@ 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")))] pub use seaography_macros::{CustomEnum, CustomFields}; +/// ``` +/// 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 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; } @@ -20,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; @@ -84,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/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), } diff --git a/src/inputs/active_enum_filter_input.rs b/src/inputs/active_enum_filter_input.rs index cbb5f837..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.to_string()); + let variant = format_variant(&variant.inner()); variant.eq(input) }); Ok(variant 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..3c02c3ec 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, @@ -57,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/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/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/lib.rs b/src/lib.rs index 5d78b7b1..632320e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,8 @@ //! //! Seaography logo //! -//!

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

+//!

🧭 A GraphQL framework for Rust

+//!

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) @@ -14,16 +13,19 @@ //! //! # 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. +//! ## Introduction //! -//! Seaography enables you to focus on your application logic instead of boilerplate. -//! With Seaography, you can: +//! 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. //! -//! + 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 +//! With Seaography you can focus on application logic instead of boilerplate. It enables you to: +//! +//! + 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 +//! + Use RBAC, guards, and lifecycle hooks to implement authorization and custom business logic //! //! ## Supported technologies //! @@ -36,35 +38,45 @@ //! //! ### 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) //! -//! + [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) +//! ### 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 | -//! |----------------------------------------------------------|-------------------------------------------------------| -//! | [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! //! //! ### 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 @@ -75,7 +87,7 @@ //! ```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 //! ``` //! @@ -87,7 +99,7 @@ //! ```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 //! ``` //! @@ -98,7 +110,7 @@ //! ```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 //! ``` //! @@ -234,7 +246,7 @@ //! } //! ``` //! -//! ### Filter using enumeration +//! ### Filter using MySQL / Postgres enum //! ```graphql //! { //! film( @@ -243,6 +255,7 @@ //! ) { //! nodes { //! filmId +//! title //! rating //! } //! } @@ -296,6 +309,13 @@ pub use builder::*; pub mod error; pub use error::*; +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>; 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..b0765319 100644 --- a/src/mutation/entity_create_batch_mutation.rs +++ b/src/mutation/entity_create_batch_mutation.rs @@ -4,8 +4,8 @@ use sea_orm::{ }; use crate::{ - apply_guard, guard_error, prepare_active_model, BuilderContext, EntityInputBuilder, - EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, + guard_error, prepare_active_model, BuilderContext, DatabaseContext, EntityInputBuilder, + EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityCreateBatchMutationBuilder @@ -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, @@ -71,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( @@ -81,16 +79,16 @@ 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) { 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 }; @@ -105,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) { @@ -120,11 +110,23 @@ 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 1b31ea3c..58321156 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, + guard_error, BuilderContext, DatabaseContext, EntityInputBuilder, EntityObjectBuilder, + EntityQueryFieldBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityCreateOneMutationBuilder @@ -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, @@ -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) { @@ -93,21 +88,12 @@ 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)?; 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) { @@ -115,12 +101,25 @@ impl EntityCreateOneMutationBuilder { } } - let active_model = prepare_active_model::( + let db = &ctx + .data::()? + .restricted(ctx.data_opt::())?; + + 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 @@ -146,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 63532741..36b0073e 100644 --- a/src/mutation/entity_delete_mutation.rs +++ b/src/mutation/entity_delete_mutation.rs @@ -4,8 +4,8 @@ use sea_orm::{ }; use crate::{ - apply_guard, get_filter_conditions, guard_error, BuilderContext, EntityObjectBuilder, - EntityQueryFieldBuilder, FilterInputBuilder, GuardAction, OperationType, + get_filter_conditions, guard_error, BuilderContext, DatabaseContext, EntityObjectBuilder, + EntityQueryFieldBuilder, FilterInputBuilder, GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityDeleteMutationBuilder @@ -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, @@ -71,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( @@ -81,16 +79,15 @@ 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) { 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..1fd67c3f 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -1,13 +1,13 @@ 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::{ - apply_guard, get_filter_conditions, guard_error, prepare_active_model, BuilderContext, + get_filter_conditions, guard_error, prepare_active_model, BuilderContext, DatabaseContext, EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, - GuardAction, OperationType, + GuardAction, OperationType, UserContext, }; /// The configuration structure of EntityUpdateMutationBuilder @@ -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, @@ -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,16 +89,16 @@ 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) { 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 }; @@ -116,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) { @@ -137,18 +126,29 @@ 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 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 = - T::find().filter(filter_condition).all(&transaction).await?; + 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? + }; + + for model in result.iter() { + A::after_save(model.clone(), &transaction, false).await?; + } transaction.commit().await?; diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index 9bd66951..4391874f 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 { @@ -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 @@ -142,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 @@ -159,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 8e08f3f1..bb6780f3 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -3,13 +3,13 @@ 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, 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, + 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 @@ -19,25 +19,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 }; @@ -47,37 +59,17 @@ 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( - 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 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(); + let relation_definition = relation_definition.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) { @@ -105,16 +97,28 @@ 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); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; + let key = KeyComplex:: { - key: vec![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, }, }; @@ -127,19 +131,18 @@ impl EntityObjectRelationBuilder { Ok(None) } }) - }), - true => Field::new( + }) + } else { + Field::new( name, TypeRef::named_nn(connection_object_builder.type_name(&object_name)), move |ctx| { 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) = 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) { @@ -167,16 +170,28 @@ 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); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; + let key = KeyComplex:: { - key: vec![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, }, }; @@ -193,10 +208,10 @@ impl EntityObjectRelationBuilder { Ok(Some(FieldValue::owned_any(connection))) }) }, - ), + ) }; - 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 135b8a08..61df0beb 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -4,14 +4,14 @@ use async_graphql::{ }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ - ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, Related, + DatabaseConnection, EntityTrait, QueryFilter, QueryTrait, Related, RelationDef, RelationType, }; use crate::{ - apply_guard, apply_memory_pagination, apply_order, apply_pagination, get_filter_conditions, - guard_error, pluralize_unique, BuilderContext, ConnectionObjectBuilder, EntityObjectBuilder, + 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, + OperationType, OrderInputBuilder, PaginationInputBuilder, UserContext, }; /// This builder produces a GraphQL field for an SeaORM entity related trait @@ -21,34 +21,45 @@ 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_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 }; @@ -58,37 +69,18 @@ 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( - 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 field_name = name.clone(); - let field = match via_relation_definition.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(); + let to_rel_def = to_rel_def.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) { @@ -109,27 +101,32 @@ 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) { 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); let order_by = OrderInputBuilder { context }.parse_object::(order_by)?; + let key = KeyComplex:: { - key: vec![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, }, }; @@ -142,18 +139,18 @@ impl EntityObjectViaRelationBuilder { Ok(None) } }) - }), - true => Field::new( + }) + } else { + Field::new( name, TypeRef::named_nn(connection_object_builder.type_name(&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(); + let via_rel_def = via_rel_def.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) { @@ -175,11 +172,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) { @@ -196,40 +189,60 @@ 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 - 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::>>(); + let loader = ctx.data_unchecked::>>(); - let key = KeyComplex:: { - key: vec![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))) }) }, - ), + ) }; - match via_relation_definition.is_owner { + match via_rel_def_is_owner { false => field, true => field .argument(InputValue::new( diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index 231adf14..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_guard, apply_order, apply_pagination, get_filter_conditions, guard_error, - pluralize_unique, BuilderContext, ConnectionObjectBuilder, EntityColumnId, EntityObjectBuilder, - FilterInputBuilder, GuardAction, OperationType, OrderInputBuilder, PaginationInput, - PaginationInputBuilder, + 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" @@ -47,7 +50,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, } } @@ -97,8 +100,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 +123,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) { @@ -143,7 +141,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?; @@ -166,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, }; @@ -180,7 +183,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); @@ -188,9 +190,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) { @@ -199,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); @@ -212,9 +213,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))) }) @@ -223,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..de625633 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)?)) }, )?; @@ -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) } diff --git a/src/query/having.rs b/src/query/having.rs new file mode 100644 index 00000000..21be8948 --- /dev/null +++ b/src/query/having.rs @@ -0,0 +1,167 @@ +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() { + // 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 { + 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/loader.rs b/src/query/loader.rs index a9422b07..c4721160 100644 --- a/src/query/loader.rs +++ b/src/query/loader.rs @@ -1,169 +1,40 @@ -use sea_orm::{sea_query::ValueTuple, Condition, ModelTrait, QueryFilter}; -use std::{collections::HashMap, hash::Hash, marker::PhantomData, sync::Arc}; +mod impl_traits; +pub(crate) mod loader_impl; + +use loader_impl::*; + +use sea_orm::{sea_query::ValueTuple, EntityTrait, QueryFilter, RelationDef}; +use std::{collections::HashMap, hash::Hash, marker::PhantomData}; 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, - /// 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)>, } -impl PartialEq for HashableGroupKey -where - T: sea_orm::EntityTrait, -{ - 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)) - } -} - -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 { - format!("{:?}", self.0).eq(&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 OneToManyLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, { connection: sea_orm::DatabaseConnection, entity: PhantomData, @@ -171,7 +42,7 @@ where impl OneToManyLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { pub fn new(connection: sea_orm::DatabaseConnection) -> Self { @@ -184,7 +55,7 @@ where impl async_graphql::dataloader::Loader> for OneToManyLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { type Value = Vec; @@ -192,88 +63,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, Vec)| { - 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 tuple = - sea_orm::sea_query::Expr::tuple(key.columns.iter().map( - |column: &T::Column| sea_orm::sea_query::Expr::col(*column).into(), - )); - 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)) - }, - ) - .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: key - .columns - .iter() - .map(|col: &T::Column| item.get(*col)) - .collect(), - 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, + ); } } @@ -283,7 +100,7 @@ where pub struct OneToOneLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, { connection: sea_orm::DatabaseConnection, entity: PhantomData, @@ -291,7 +108,7 @@ where impl OneToOneLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { pub fn new(connection: sea_orm::DatabaseConnection) -> Self { @@ -304,7 +121,7 @@ where impl async_graphql::dataloader::Loader> for OneToOneLoader where - T: sea_orm::EntityTrait, + T: EntityTrait, T::Model: Sync, { type Value = T::Model; @@ -312,84 +129,58 @@ 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, Vec)| { - 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 tuple = - sea_orm::sea_query::Expr::tuple(key.columns.iter().map( - |column: &T::Column| sea_orm::sea_query::Expr::col(*column).into(), - )); - 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)) - }, - ) - .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: key - .columns - .iter() - .map(|col: &T::Column| item.get(*col)) - .collect(), - 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, + ); + } } } Ok(results) } } + +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 new file mode 100644 index 00000000..24d7520f --- /dev/null +++ b/src/query/loader/impl_traits.rs @@ -0,0 +1,113 @@ +use super::*; + +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); + } +} + +impl PartialEq for HashableGroupKey +where + T: sea_orm::EntityTrait, +{ + fn eq(&self, other: &Self) -> bool { + 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), + ) + } +} + +impl Eq for HashableGroupKey where T: sea_orm::EntityTrait {} + +impl Hash for HashableGroupKey +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.order_by).hash(state); + } +} diff --git a/src/query/loader/loader_impl.rs b/src/query/loader/loader_impl.rs new file mode 100644 index 00000000..39296875 --- /dev/null +++ b/src/query/loader/loader_impl.rs @@ -0,0 +1,260 @@ +//! Mostly copied from SeaORM +use crate::IdenIter; +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() +} 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::*; diff --git a/src/query/pagination.rs b/src/query/pagination.rs index 5c984b97..be986534 100644 --- a/src/query/pagination.rs +++ b/src/query/pagination.rs @@ -1,24 +1,25 @@ 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::{ - 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 -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); @@ -63,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); } @@ -78,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?; @@ -98,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?; @@ -115,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); @@ -150,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); @@ -191,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); @@ -204,13 +189,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, @@ -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); @@ -489,8 +469,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})" ), ))); } 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(()) + } +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 00000000..99c02869 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,244 @@ +use crate as seaography; +use crate::CustomOutputType; +use serde::Deserialize; + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] +pub struct Table { + pub columns: Vec, + pub primary_key: Vec, + pub comment: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] +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, CustomOutputType)] +pub struct ColumnType { + pub primitive: Option, + pub array: Option, + pub enumeration: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] +pub struct Array { + pub array: Box, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, CustomOutputType)] +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, + } + ) + } +} diff --git a/src/utilities.rs b/src/utilities.rs index e1c339be..35e07669 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -1,10 +1,10 @@ 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 { +pub fn encode_cursor(values: ValueTuple) -> String { values .iter() .map(|value| -> String { @@ -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() @@ -107,31 +106,37 @@ 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)); + } + } } } /// 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(); @@ -246,7 +251,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 +259,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 => { @@ -281,7 +286,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")] @@ -322,3 +329,44 @@ 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 + } +}