diff --git a/.github/workflows/discord-pr-notifications.yaml b/.github/workflows/discord-pr-notifications.yaml new file mode 100644 index 00000000..5fe96ef2 --- /dev/null +++ b/.github/workflows/discord-pr-notifications.yaml @@ -0,0 +1,17 @@ +name: Pull Request Notification to Discord + +on: + pull_request: + types: [opened, closed] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Send notification to Discord + env: + WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: | + curl -X POST -H "Content-Type: application/json" \ + -d "{\"content\": \"🔔 New Pull Request: **${{ github.event.pull_request.title }}** by ${{ github.actor }} - ${{ github.event.pull_request.html_url }}\"}" \ + $WEBHOOK_URL \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 0861f8f2..11e21f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,15 @@ keywords = ["async", "graphql", "mysql", "postgres", "sqlite"] categories = ["database"] [dependencies] -async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } -sea-orm = { version = "~1.1.0", default-features = false, features = ["seaography"] } +async-graphql = { version = "7.0", features = [ + "decimal", + "chrono", + "dataloader", + "dynamic-schema", +] } +sea-orm = { version = "1.0.*", default-features = false, features = [ + "seaography", +] } itertools = { version = "0.12.0" } heck = { version = "0.4.1" } thiserror = { version = "1.0.44" } @@ -32,6 +39,7 @@ with-uuid = ["sea-orm/with-uuid"] with-decimal = ["sea-orm/with-rust_decimal", "async-graphql/decimal"] with-bigdecimal = ["sea-orm/with-bigdecimal", "async-graphql/bigdecimal"] with-postgres-array = ["sea-orm/postgres-array"] +offset-pagination = [] # with-ipnetwork = ["sea-orm/with-ipnetwork"] # with-mac_address = ["sea-orm/with-mac_address"] field-snake-case = [] diff --git a/README.md b/README.md index 674838d1..0ba4c997 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ 🧭 A GraphQL framework and code generator for SeaORM

- [![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) - [![build status](https://github.com/SeaQL/seaography/actions/workflows/tests.yaml/badge.svg)](https://github.com/SeaQL/seaography/actions/workflows/tests.yaml) +[![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) +[![build status](https://github.com/SeaQL/seaography/actions/workflows/tests.yaml/badge.svg)](https://github.com/SeaQL/seaography/actions/workflows/tests.yaml) @@ -18,27 +18,27 @@ ## Benefits -* Quick and easy to get started -* Generates readable code -* Extensible project structure -* Based on popular async libraries: [async-graphql](https://github.com/async-graphql/async-graphql) and [SeaORM](https://github.com/SeaQL/sea-orm) +- Quick and easy to get started +- Generates readable code +- Extensible project structure +- Based on popular async libraries: [async-graphql](https://github.com/async-graphql/async-graphql) and [SeaORM](https://github.com/SeaQL/sea-orm) ## Features -* Relational query (1-to-1, 1-to-N) -* Pagination for queries and relations (1-N) -* Filtering with operators (e.g. gt, lt, eq) -* Order by any column -* Guard fields, queries or relations -* Rename fields -* Mutations (create, update, delete) +- Relational query (1-to-1, 1-to-N) +- Pagination for queries and relations (1-N) +- Filtering with operators (e.g. gt, lt, eq) +- Order by any column +- Guard fields, queries or relations +- Rename fields +- Mutations (create, update, delete) (Right now there is no mutation, but it's on our plan!) ## SeaORM Version Compatibility -| Seaography | SeaORM | -|----------------------------------------------------------|-------------------------------------------------------| +| Seaography | SeaORM | +| -------------------------------------------------------- | ----------------------------------------------------- | | [1.1-rc](https://crates.io/crates/seaography/1.1.0-rc.1) | [1.1-rc](https://crates.io/crates/sea-orm/1.1.0-rc.1) | | [1.0](https://crates.io/crates/seaography/1.0.0) | [1.0](https://crates.io/crates/sea-orm/1.0.0) | | [0.12](https://crates.io/crates/seaography/0.12.0) | [0.12](https://crates.io/crates/sea-orm/0.12.14) | @@ -70,19 +70,22 @@ Go to http://localhost:8000/ and try out the following queries: ```graphql { - film(pagination: { page: { limit: 10, page: 0 } }, orderBy: { title: ASC }) { - nodes { - title - description - releaseYear - actor { + film( + pagination: { page: { limit: 10, page: 0 } } + orderBy: { title: ASC } + ) { nodes { - firstName - lastName + title + description + releaseYear + actor { + nodes { + firstName + lastName + } + } } - } } - } } ``` @@ -90,19 +93,19 @@ Go to http://localhost:8000/ and try out the following queries: ```graphql { - store(filters: { storeId: { eq: 1 } }) { - nodes { - storeId - address { - address - address2 - } - staff { - firstName - lastName - } + store(filters: { storeId: { eq: 1 } }) { + nodes { + storeId + address { + address + address2 + } + staff { + firstName + lastName + } + } } - } } ``` @@ -110,20 +113,20 @@ Go to http://localhost:8000/ and try out the following queries: ```graphql { - customer( - filters: { active: { eq: 0 } } - pagination: { page: { page: 2, limit: 3 } } - ) { - nodes { - customerId - lastName - email - } - paginationInfo { - pages - current + customer( + filters: { active: { eq: 0 } } + pagination: { page: { page: 2, limit: 3 } } + ) { + nodes { + customerId + lastName + email + } + paginationInfo { + pages + current + } } - } } ``` @@ -131,21 +134,21 @@ Go to http://localhost:8000/ and try out the following queries: ```graphql { - customer( - filters: { active: { eq: 0 } } - pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } - ) { - nodes { - customerId - lastName - email - } - pageInfo { - hasPreviousPage - hasNextPage - endCursor + customer( + filters: { active: { eq: 0 } } + pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } + ) { + nodes { + customerId + lastName + email + } + pageInfo { + hasPreviousPage + hasNextPage + endCursor + } } - } } ``` @@ -155,57 +158,58 @@ Find all inactive customers, include their address, and their payments with amou ```graphql { - customer( - filters: { active: { eq: 0 } } - pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } - ) { - nodes { - customerId - lastName - email - address { - address - } - payment( - filters: { amount: { gt: "7" } } - orderBy: { amount: ASC } - pagination: { page: { limit: 1, page: 1 } } - ) { + customer( + filters: { active: { eq: 0 } } + pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } + ) { nodes { - paymentId - amount - } - paginationInfo { - pages - current + customerId + lastName + email + address { + address + } + payment( + filters: { amount: { gt: "7" } } + orderBy: { amount: ASC } + pagination: { page: { limit: 1, page: 1 } } + ) { + nodes { + paymentId + amount + } + paginationInfo { + pages + current + } + pageInfo { + hasPreviousPage + hasNextPage + } + } } pageInfo { - hasPreviousPage - hasNextPage + hasPreviousPage + hasNextPage + endCursor } - } } - pageInfo { - hasPreviousPage - hasNextPage - endCursor - } - } } ``` ### Filter using enumeration + ```graphql { - film( - filters: { rating: { eq: NC17 } } - pagination: { page: { page: 1, limit: 5 } } - ) { - nodes { - filmId - rating + film( + filters: { rating: { eq: NC17 } } + pagination: { page: { page: 1, limit: 5 } } + ) { + nodes { + filmId + rating + } } - } } ``` @@ -233,4 +237,4 @@ cargo run Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -Seaography is a community driven project. We welcome you to participate, contribute and together build for Rust's future. +Seaography is a community driven project. We welcome you to participate, contribute and together build for Rust's future. \ No newline at end of file diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index 1b423ec8..7ff88ef2 100644 --- a/examples/mysql/Cargo.toml +++ b/examples/mysql/Cargo.toml @@ -22,5 +22,8 @@ features = ["with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } +[features] +offset-pagination = ["seaography/offset-pagination"] + [workspace] -members = [] +members = [] \ No newline at end of file diff --git a/examples/mysql/tests/mutation_tests.rs b/examples/mysql/tests/mutation_tests.rs index 44a1948d..f638ddba 100644 --- a/examples/mysql/tests/mutation_tests.rs +++ b/examples/mysql/tests/mutation_tests.rs @@ -1,6 +1,7 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; +#[cfg(not(feature = "offset-pagination"))] async fn main() { test_simple_insert_one().await; test_complex_insert_one().await; @@ -620,4 +621,4 @@ async fn test_delete_mutation() { } "#, ); -} +} \ No newline at end of file diff --git a/examples/mysql/tests/offset_pagination_query_tests.rs b/examples/mysql/tests/offset_pagination_query_tests.rs new file mode 100644 index 00000000..d966d07e --- /dev/null +++ b/examples/mysql/tests/offset_pagination_query_tests.rs @@ -0,0 +1,137 @@ +use async_graphql::{dynamic::*, Response}; +use sea_orm::Database; + +pub async fn get_schema() -> Schema { + let database = Database::connect("mysql://sea:sea@127.0.0.1/sakila") + .await + .unwrap(); + let schema = seaography_mysql_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() + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_simple_query() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + store { + storeId + staff { + firstName + lastName + } + } + } + "#, + ) + .await, + r#" + { + "store": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" + } + }, + { + "storeId": 2, + "staff": { + "firstName": "Jon", + "lastName": "Stephens" + } + } + ] + } + "#, + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_simple_query_with_filter() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + store(filters: {storeId:{eq: 1}}) { + storeId + staff { + firstName + lastName + } + } + } + "#, + ) + .await, + r#" + { + "store": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" + } + } + ] + } + "#, + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_filter_with_pagination() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + customer( + filters: { active: { eq: 0 } } + pagination: { page: { page: 2, limit: 3 } } + ) { + customerId + } + } + "#, + ) + .await, + r#" + { + "customer": [ + { + "customerId": 315 + }, + { + "customerId": 368 + }, + { + "customerId": 406 + } + ] + } + "#, + ) +} \ No newline at end of file diff --git a/examples/mysql/tests/query_tests.rs b/examples/mysql/tests/query_tests.rs index 97ca5a8c..d6b5ffbd 100644 --- a/examples/mysql/tests/query_tests.rs +++ b/examples/mysql/tests/query_tests.rs @@ -17,6 +17,7 @@ pub fn assert_eq(a: Response, b: &str) { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_simple_query() { let schema = get_schema().await; @@ -64,6 +65,7 @@ async fn test_simple_query() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_simple_query_with_filter() { let schema = get_schema().await; @@ -104,6 +106,7 @@ async fn test_simple_query_with_filter() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_filter_with_pagination() { let schema = get_schema().await; @@ -153,6 +156,7 @@ async fn test_filter_with_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_complex_filter_with_pagination() { let schema = get_schema().await; @@ -202,6 +206,7 @@ async fn test_complex_filter_with_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination() { let schema = get_schema().await; @@ -297,6 +302,7 @@ async fn test_cursor_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination_prev() { let schema = get_schema().await; @@ -374,6 +380,7 @@ async fn test_cursor_pagination_prev() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination_no_next() { let schema = get_schema().await; @@ -442,6 +449,7 @@ async fn test_cursor_pagination_no_next() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_self_ref() { let schema = get_schema().await; @@ -506,6 +514,7 @@ async fn test_self_ref() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn related_queries_filters() { let schema = get_schema().await; @@ -696,6 +705,7 @@ async fn related_queries_filters() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn related_queries_pagination() { let schema = get_schema().await; @@ -833,6 +843,7 @@ async fn related_queries_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn enumeration_filter() { let schema = get_schema().await; @@ -884,4 +895,4 @@ async fn enumeration_filter() { } "#, ) -} +} \ No newline at end of file diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml index c6539f5e..653dc1ea 100644 --- a/examples/postgres/Cargo.toml +++ b/examples/postgres/Cargo.toml @@ -22,5 +22,8 @@ features = ["with-decimal", "with-chrono", "with-postgres-array"] [dev-dependencies] serde_json = { version = "1.0.103" } +[features] +offset-pagination = ["seaography/offset-pagination"] + [workspace] -members = [] +members = [] \ No newline at end of file diff --git a/examples/postgres/tests/mutation_tests.rs b/examples/postgres/tests/mutation_tests.rs index 1fa66e91..2cff0963 100644 --- a/examples/postgres/tests/mutation_tests.rs +++ b/examples/postgres/tests/mutation_tests.rs @@ -1,6 +1,7 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn main() { test_simple_insert_one().await; @@ -630,4 +631,4 @@ async fn test_delete_mutation() { } "#, ); -} +} \ No newline at end of file diff --git a/examples/postgres/tests/offset_pagination_query_tests.rs b/examples/postgres/tests/offset_pagination_query_tests.rs new file mode 100644 index 00000000..5285ec02 --- /dev/null +++ b/examples/postgres/tests/offset_pagination_query_tests.rs @@ -0,0 +1,137 @@ +use async_graphql::{dynamic::*, Response}; +use sea_orm::Database; + +pub async fn get_schema() -> Schema { + let database = Database::connect("postgres://sea:sea@127.0.0.1/sakila") + .await + .unwrap(); + let schema = seaography_postgres_example::query_root::schema(database, None, None).unwrap(); + + schema +} + +pub fn assert_eq(a: Response, b: &str) { + assert_eq!( + a.data.into_json().unwrap(), + serde_json::from_str::(b).unwrap() + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_simple_query() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + store { + storeId + staff { + firstName + lastName + } + } + } + "#, + ) + .await, + r#" + { + "store": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" + } + }, + { + "storeId": 2, + "staff": { + "firstName": "Jon", + "lastName": "Stephens" + } + } + ] + } + "#, + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_simple_query_with_filter() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + store(filters: {storeId:{eq: 1}}) { + storeId + staff { + firstName + lastName + } + } + } + "#, + ) + .await, + r#" + { + "store": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" + } + } + ] + } + "#, + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_filter_with_pagination() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + customer( + filters: { active: { eq: 0 } } + pagination: { page: { page: 2, limit: 3 } } + ) { + customerId + } + } + "#, + ) + .await, + r#" + { + "customer": [ + { + "customerId": 315 + }, + { + "customerId": 368 + }, + { + "customerId": 406 + } + ] + } + "#, + ) +} \ No newline at end of file diff --git a/examples/postgres/tests/query_tests.rs b/examples/postgres/tests/query_tests.rs index 7c3d340d..c69ecb93 100644 --- a/examples/postgres/tests/query_tests.rs +++ b/examples/postgres/tests/query_tests.rs @@ -17,6 +17,7 @@ pub fn assert_eq(a: Response, b: &str) { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_simple_query() { let schema = get_schema().await; @@ -64,6 +65,7 @@ async fn test_simple_query() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_simple_query_with_filter() { let schema = get_schema().await; @@ -104,6 +106,7 @@ async fn test_simple_query_with_filter() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_filter_with_pagination() { let schema = get_schema().await; @@ -153,6 +156,7 @@ async fn test_filter_with_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_complex_filter_with_pagination() { let schema = get_schema().await; @@ -202,6 +206,7 @@ async fn test_complex_filter_with_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination() { let schema = get_schema().await; @@ -297,6 +302,7 @@ async fn test_cursor_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination_prev() { let schema = get_schema().await; @@ -374,6 +380,7 @@ async fn test_cursor_pagination_prev() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination_no_next() { let schema = get_schema().await; @@ -507,6 +514,7 @@ async fn test_cursor_pagination_no_next() { // ) // } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn related_queries_filters() { let schema = get_schema().await; @@ -694,6 +702,7 @@ async fn related_queries_filters() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn related_queries_pagination() { let schema = get_schema().await; @@ -831,6 +840,7 @@ async fn related_queries_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn enumeration_filter() { let schema = get_schema().await; @@ -884,6 +894,7 @@ async fn enumeration_filter() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_boolean_field() { let schema = get_schema().await; @@ -916,4 +927,4 @@ async fn test_boolean_field() { } "#, ) -} +} \ No newline at end of file diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 39338e1c..11e70c60 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -22,5 +22,8 @@ features = ["with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } +[features] +offset-pagination = ["seaography/offset-pagination"] + [workspace] -members = [] +members = [] \ No newline at end of file diff --git a/examples/sqlite/tests/guard_mutation_tests.rs b/examples/sqlite/tests/guard_mutation_tests.rs index ced4915a..c1eee029 100644 --- a/examples/sqlite/tests/guard_mutation_tests.rs +++ b/examples/sqlite/tests/guard_mutation_tests.rs @@ -80,7 +80,7 @@ pub fn assert_eq(a: Response, b: &str) { serde_json::from_str::(b).unwrap() ) } - +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn entity_guard_mutation() { let schema = get_schema().await; @@ -130,6 +130,7 @@ async fn entity_guard_mutation() { assert_eq!(response.errors[0].message, "Entity guard triggered."); } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn field_guard_mutation() { let schema = get_schema().await; @@ -141,7 +142,7 @@ async fn field_guard_mutation() { languageUpdate(data: { name: "Cantonese" }, filter: { languageId: { eq: 6 } }) { languageId } - } + } "#, ) .await; @@ -149,4 +150,4 @@ async fn field_guard_mutation() { assert_eq!(response.errors.len(), 1); assert_eq!(response.errors[0].message, "Field guard triggered."); -} +} \ No newline at end of file diff --git a/examples/sqlite/tests/guard_tests.rs b/examples/sqlite/tests/guard_tests.rs index 9e9340a6..0d75ff02 100644 --- a/examples/sqlite/tests/guard_tests.rs +++ b/examples/sqlite/tests/guard_tests.rs @@ -285,6 +285,7 @@ pub fn assert_eq(a: Response, b: &str) { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn entity_guard() { let schema = get_schema().await; @@ -357,6 +358,7 @@ async fn entity_guard() { assert_eq!(response.errors[0].message, "Entity guard triggered."); } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn field_guard() { let schema = get_schema().await; @@ -380,4 +382,4 @@ async fn field_guard() { assert_eq!(response.errors.len(), 1); assert_eq!(response.errors[0].message, "Field guard triggered."); -} +} \ No newline at end of file diff --git a/examples/sqlite/tests/mutation_tests.rs b/examples/sqlite/tests/mutation_tests.rs index c3f9dec5..53be711d 100644 --- a/examples/sqlite/tests/mutation_tests.rs +++ b/examples/sqlite/tests/mutation_tests.rs @@ -1,6 +1,7 @@ use async_graphql::{dynamic::*, Response}; use sea_orm::Database; +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn main() { test_simple_insert_one().await; @@ -24,6 +25,7 @@ pub fn assert_eq(a: Response, b: &str) { ) } +#[cfg(not(feature = "offset-pagination"))] async fn test_simple_insert_one() { let schema = get_schema().await; @@ -106,6 +108,7 @@ async fn test_simple_insert_one() { ); } +#[cfg(not(feature = "offset-pagination"))] async fn test_complex_insert_one() { let schema = get_schema().await; @@ -218,6 +221,7 @@ async fn test_complex_insert_one() { ); } +#[cfg(not(feature = "offset-pagination"))] async fn test_create_batch_mutation() { let schema = get_schema().await; @@ -320,6 +324,7 @@ async fn test_create_batch_mutation() { ); } +#[cfg(not(feature = "offset-pagination"))] async fn test_update_mutation() { let schema = get_schema().await; @@ -498,6 +503,7 @@ async fn test_update_mutation() { ); } +#[cfg(not(feature = "offset-pagination"))] async fn test_delete_mutation() { let schema = get_schema().await; @@ -601,4 +607,4 @@ async fn test_delete_mutation() { } "#, ); -} +} \ No newline at end of file diff --git a/examples/sqlite/tests/offset_pagination_query_tests.rs b/examples/sqlite/tests/offset_pagination_query_tests.rs new file mode 100644 index 00000000..bf4ac006 --- /dev/null +++ b/examples/sqlite/tests/offset_pagination_query_tests.rs @@ -0,0 +1,135 @@ +use async_graphql::{dynamic::*, Response}; +use sea_orm::Database; + +pub async fn get_schema() -> Schema { + let database = Database::connect("sqlite://sakila.db").await.unwrap(); + let schema = seaography_sqlite_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() + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_simple_query() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + store { + storeId + staff { + firstName + lastName + } + } + } + "#, + ) + .await, + r#" + { + "store": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" + } + }, + { + "storeId": 2, + "staff": { + "firstName": "Jon", + "lastName": "Stephens" + } + } + ] + } + "#, + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_simple_query_with_filter() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + store(filters: {storeId:{eq: 1}}) { + storeId + staff { + firstName + lastName + } + } + } + "#, + ) + .await, + r#" + { + "store": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" + } + } + ] + } + "#, + ) +} + +#[cfg(feature = "offset-pagination")] +#[tokio::test] +async fn test_filter_with_pagination() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + customer( + filters: { active: { eq: 0 } } + pagination: { page: { page: 2, limit: 3 } } + ) { + customerId + } + } + "#, + ) + .await, + r#" + { + "customer": [ + { + "customerId": 315 + }, + { + "customerId": 368 + }, + { + "customerId": 406 + } + ] + } + "#, + ) +} \ No newline at end of file diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index 0f04935e..4104de1e 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -15,6 +15,7 @@ pub fn assert_eq(a: Response, b: &str) { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_simple_query() { let schema = get_schema().await; @@ -62,6 +63,7 @@ async fn test_simple_query() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_simple_query_with_filter() { let schema = get_schema().await; @@ -102,6 +104,7 @@ async fn test_simple_query_with_filter() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_filter_with_pagination() { let schema = get_schema().await; @@ -151,6 +154,7 @@ async fn test_filter_with_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_complex_filter_with_pagination() { let schema = get_schema().await; @@ -200,6 +204,7 @@ async fn test_complex_filter_with_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination() { let schema = get_schema().await; @@ -295,6 +300,7 @@ async fn test_cursor_pagination() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination_prev() { let schema = get_schema().await; @@ -372,6 +378,7 @@ async fn test_cursor_pagination_prev() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_cursor_pagination_no_next() { let schema = get_schema().await; @@ -440,6 +447,7 @@ async fn test_cursor_pagination_no_next() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn test_self_ref() { let schema = get_schema().await; @@ -504,6 +512,7 @@ async fn test_self_ref() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn related_queries_filters() { let schema = get_schema().await; @@ -706,6 +715,7 @@ async fn related_queries_filters() { ) } +#[cfg(not(feature = "offset-pagination"))] #[tokio::test] async fn related_queries_pagination() { let schema = get_schema().await; @@ -841,4 +851,4 @@ async fn related_queries_pagination() { } "#, ) -} +} \ No newline at end of file diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml index c0fc0236..7e892926 100644 --- a/generator/src/templates/actix_cargo.toml +++ b/generator/src/templates/actix_cargo.toml @@ -21,5 +21,8 @@ features = ["with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } +[features] +offset-pagination = ["seaography/offset-pagination"] + [workspace] -members = [] +members = [] \ No newline at end of file diff --git a/generator/src/templates/axum_cargo.toml b/generator/src/templates/axum_cargo.toml index 37f2d00e..728b4064 100644 --- a/generator/src/templates/axum_cargo.toml +++ b/generator/src/templates/axum_cargo.toml @@ -21,5 +21,8 @@ features = ["with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } +[features] +offset-pagination = ["seaography/offset-pagination"] + [workspace] -members = [] +members = [] \ No newline at end of file diff --git a/generator/src/templates/poem_cargo.toml b/generator/src/templates/poem_cargo.toml index 3313908c..32a0250d 100644 --- a/generator/src/templates/poem_cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -21,5 +21,8 @@ features = ["with-decimal", "with-chrono"] [dev-dependencies] serde_json = { version = "1.0.103" } +[features] +offset-pagination = ["seaography/offset-pagination"] + [workspace] -members = [] +members = [] \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index 592f5041..f24dcd7a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,15 +1,19 @@ use async_graphql::{ dataloader::DataLoader, - dynamic::{Enum, Field, FieldFuture, InputObject, Object, Schema, SchemaBuilder, TypeRef}, + dynamic::{ + Enum, Field, FieldFuture, InputObject, Interface, Object, Schema, SchemaBuilder, TypeRef, + ValueAccessor, + }, }; -use sea_orm::{ActiveEnum, ActiveModelTrait, EntityTrait, IntoActiveModel}; +use sea_orm::{ActiveEnum, ActiveModelTrait, EntityTrait, IntoActiveModel, RelationDef}; use crate::{ - ActiveEnumBuilder, ActiveEnumFilterInputBuilder, BuilderContext, ConnectionObjectBuilder, - CursorInputBuilder, EdgeObjectBuilder, EntityCreateBatchMutationBuilder, - EntityCreateOneMutationBuilder, EntityDeleteMutationBuilder, EntityInputBuilder, - EntityObjectBuilder, EntityQueryFieldBuilder, EntityUpdateMutationBuilder, FilterInputBuilder, - FilterTypesMapHelper, OffsetInputBuilder, OneToManyLoader, OneToOneLoader, OrderByEnumBuilder, + ActiveEnumBuilder, ActiveEnumFilterInputBuilder, BuilderContext, CascadeInputBuilder, + ConnectionObjectBuilder, CursorInputBuilder, EdgeObjectBuilder, + EntityCreateBatchMutationBuilder, EntityCreateOneMutationBuilder, EntityDeleteMutationBuilder, + EntityGetFieldBuilder, EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, + EntityUpdateMutationBuilder, FilterInputBuilder, FilterTypesMapHelper, NewOrderInputBuilder, + OffsetInputBuilder, OneToManyLoader, OneToOneLoader, OrderByEnumBuilder, OrderEnumBuilder, OrderInputBuilder, PageInfoObjectBuilder, PageInputBuilder, PaginationInfoObjectBuilder, PaginationInputBuilder, }; @@ -25,6 +29,9 @@ pub struct Builder { /// holds all output object types pub outputs: Vec, + // holds all interfaces object types + pub interfaces: Vec, + /// holds all input object types pub inputs: Vec, @@ -60,6 +67,7 @@ impl Builder { mutation, schema, outputs: Vec::new(), + interfaces: Vec::new(), inputs: Vec::new(), enumerations: Vec::new(), queries: Vec::new(), @@ -70,7 +78,7 @@ impl Builder { } /// used to register a new entity to the Builder context - pub fn register_entity(&mut self, relations: Vec) + pub fn register_entity(&mut self, relations: Vec, interfaces: Vec<&str>) where T: EntityTrait, ::Model: Sync, @@ -82,35 +90,68 @@ impl Builder { entity_object_builder.to_object::(), |entity_object, field| entity_object.field(field), ); + let entity_object = interfaces + .into_iter() + .fold(entity_object, |entity_object, interface| { + entity_object.implement(interface) + }); + + if cfg!(feature = "offset-pagination") { + self.outputs.extend(vec![entity_object]); + } else { + let edge_object_builder = EdgeObjectBuilder { + context: self.context, + }; - let edge_object_builder = EdgeObjectBuilder { - context: self.context, - }; - let edge = edge_object_builder.to_object::(); + let edge = edge_object_builder.to_object::(); - let connection_object_builder = ConnectionObjectBuilder { - context: self.context, - }; - let connection = connection_object_builder.to_object::(); + let connection_object_builder = ConnectionObjectBuilder { + context: self.context, + }; + + let connection = connection_object_builder.to_object::(); - self.outputs.extend(vec![entity_object, edge, connection]); + self.outputs.extend(vec![entity_object, edge, connection]); + } let filter_input_builder = FilterInputBuilder { context: self.context, }; let filter = filter_input_builder.to_object::(); + let cascade_input_builder = CascadeInputBuilder { + context: self.context, + }; + let cascade = cascade_input_builder.to_object::(); + let order_input_builder = OrderInputBuilder { context: self.context, }; let order = order_input_builder.to_object::(); - self.inputs.extend(vec![filter, order]); + + let new_order_input_builder = NewOrderInputBuilder { + context: self.context, + }; + let new_order = new_order_input_builder.to_object::(); + self.inputs.extend(vec![filter, order, new_order, cascade]); + + let order_enum_builder = OrderEnumBuilder { + context: self.context, + }; + let order_enum = order_enum_builder.enumeration::(); + self.enumerations.push(order_enum); let entity_query_field_builder = EntityQueryFieldBuilder { context: self.context, }; let query = entity_query_field_builder.to_field::(); self.queries.push(query); + + let entity_get_field_builder = EntityGetFieldBuilder { + context: self.context, + }; + let get_query = entity_get_field_builder.to_field::(); + self.queries.push(get_query); } pub fn register_entity_mutations(&mut self) @@ -237,6 +278,12 @@ impl Builder { .into_iter() .fold(mutation, |mutation, field| mutation.field(field)); + // register interfaces to schema + let schema = self + .interfaces + .into_iter() + .fold(schema, |schema, interface| schema.register(interface)); + // register entities to schema let schema = self .outputs @@ -271,12 +318,8 @@ impl Builder { } .enumeration(), ) - .register( - CursorInputBuilder { - context: self.context, - } - .input_object(), - ) + .register(query) + .register(mutation) .register( CursorInputBuilder { context: self.context, @@ -313,8 +356,6 @@ impl Builder { } .to_object(), ) - .register(query) - .register(mutation) } } @@ -325,13 +366,22 @@ pub trait RelationBuilder { ) -> async_graphql::dynamic::Field; } +pub trait CascadeBuilder { + fn get_join( + &self, + context: &'static crate::BuilderContext, + filters: Option, + ) -> RelationDef; +} + #[macro_export] macro_rules! register_entity { - ($builder:expr, $module_path:ident) => { + ($builder:expr, $module_path:ident,$interfaces:expr) => { $builder.register_entity::<$module_path::Entity>( <$module_path::RelatedEntity as sea_orm::Iterable>::iter() .map(|rel| seaography::RelationBuilder::get_relation(&rel, $builder.context)) .collect(), + $interfaces, ); $builder = $builder.register_entity_dataloader_one_to_one($module_path::Entity, tokio::spawn); @@ -343,8 +393,8 @@ macro_rules! register_entity { #[macro_export] macro_rules! register_entities { - ($builder:expr, [$($module_paths:ident),+ $(,)?]) => { - $(seaography::register_entity!($builder, $module_paths);)* + ($builder:expr, [$(($module_paths:ident,$interfaces:expr)),+ $(,)?]) => { + $(seaography::register_entity!($builder, $module_paths,$interfaces);)* }; } diff --git a/src/builder_context.rs b/src/builder_context.rs index 7beaf99c..72f8ee68 100644 --- a/src/builder_context.rs +++ b/src/builder_context.rs @@ -1,8 +1,9 @@ use crate::{ - ActiveEnumConfig, ActiveEnumFilterInputConfig, ConnectionObjectConfig, CursorInputConfig, - EdgeObjectConfig, EntityCreateBatchMutationConfig, EntityCreateOneMutationConfig, - EntityDeleteMutationConfig, EntityInputConfig, EntityObjectConfig, EntityQueryFieldConfig, - EntityUpdateMutationConfig, FilterInputConfig, OffsetInputConfig, OrderByEnumConfig, + ActiveEnumConfig, ActiveEnumFilterInputConfig, CascadeInputConfig, ConnectionObjectConfig, + CursorInputConfig, EdgeObjectConfig, EntityCreateBatchMutationConfig, + EntityCreateOneMutationConfig, EntityDeleteMutationConfig, EntityGetFieldConfig, + EntityInputConfig, EntityObjectConfig, EntityQueryFieldConfig, EntityUpdateMutationConfig, + FilterInputConfig, NewOrderInputConfig, OffsetInputConfig, OrderByEnumConfig, OrderEnumConfig, OrderInputConfig, PageInfoObjectConfig, PageInputConfig, PaginationInfoObjectConfig, PaginationInputConfig, }; @@ -23,6 +24,7 @@ pub use filter_types_map::*; #[derive(Default)] pub struct BuilderContext { pub order_by_enum: OrderByEnumConfig, + pub order_enum: OrderEnumConfig, pub active_enum: ActiveEnumConfig, pub cursor_input: CursorInputConfig, @@ -31,8 +33,10 @@ pub struct BuilderContext { pub pagination_input: PaginationInputConfig, pub order_input: OrderInputConfig, + pub new_order_input: NewOrderInputConfig, pub filter_input: FilterInputConfig, + pub cascade_input: CascadeInputConfig, pub active_enum_filter_input: ActiveEnumFilterInputConfig, pub page_info_object: PageInfoObjectConfig, @@ -41,6 +45,7 @@ pub struct BuilderContext { pub entity_object: EntityObjectConfig, pub connection_object: ConnectionObjectConfig, pub entity_query_field: EntityQueryFieldConfig, + pub entity_get_field: EntityGetFieldConfig, pub entity_create_one_mutation: EntityCreateOneMutationConfig, pub entity_create_batch_mutation: EntityCreateBatchMutationConfig, @@ -54,4 +59,4 @@ pub struct BuilderContext { pub filter_types: FilterTypesMapConfig, // is_skipped function // naming function -} +} \ No newline at end of file diff --git a/src/builder_context/filter_types_map.rs b/src/builder_context/filter_types_map.rs index 658f19ef..f567ccef 100644 --- a/src/builder_context/filter_types_map.rs +++ b/src/builder_context/filter_types_map.rs @@ -343,10 +343,9 @@ impl FilterTypesMapHelper { FilterOperation::LessThanEquals => { InputValue::new("lte", TypeRef::named(filter_info.base_type.clone())) } - FilterOperation::IsIn => InputValue::new( - "is_in", - TypeRef::named_nn_list(filter_info.base_type.clone()), - ), + FilterOperation::IsIn => { + InputValue::new("in", TypeRef::named_nn_list(filter_info.base_type.clone())) + } FilterOperation::IsNotIn => InputValue::new( "is_not_in", TypeRef::named_nn_list(filter_info.base_type.clone()), @@ -483,7 +482,7 @@ impl FilterTypesMapHelper { } } FilterOperation::IsIn => { - if let Some(value) = filter.get("is_in") { + if let Some(value) = filter.get("in") { let value = value .list()? .iter() @@ -648,4 +647,4 @@ pub enum FilterOperation { NotLike, Between, NotBetween, -} +} \ No newline at end of file diff --git a/src/enumerations/active_enum.rs b/src/enumerations/active_enum.rs index e156afa4..f69ad770 100644 --- a/src/enumerations/active_enum.rs +++ b/src/enumerations/active_enum.rs @@ -21,6 +21,11 @@ impl std::default::Default for ActiveEnumConfig { variant_name: Box::new(|_enum_name: &str, variant: &str| -> String { if cfg!(feature = "field-snake-case") { variant.to_snake_case() + } else if cfg!(feature = "offset-pagination") { + variant + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .collect() } else { variant.to_upper_camel_case().to_ascii_uppercase() } @@ -64,4 +69,4 @@ impl ActiveEnumBuilder { enumeration.item(self.variant_name(&enum_name, &variant)) }) } -} +} \ No newline at end of file diff --git a/src/enumerations/mod.rs b/src/enumerations/mod.rs index 28e20c28..3b5d360a 100644 --- a/src/enumerations/mod.rs +++ b/src/enumerations/mod.rs @@ -3,3 +3,6 @@ pub use order_by_enum::*; pub mod active_enum; pub use active_enum::*; + +pub mod order_enum; +pub use order_enum::*; \ No newline at end of file diff --git a/src/enumerations/order_enum.rs b/src/enumerations/order_enum.rs new file mode 100644 index 00000000..2e210590 --- /dev/null +++ b/src/enumerations/order_enum.rs @@ -0,0 +1,48 @@ +use async_graphql::dynamic::{Enum, EnumItem}; +use sea_orm::{EntityTrait, Iterable}; + +use crate::{BuilderContext, EntityObjectBuilder}; + +/// The configuration structure for OrderByEnumBuilder +pub struct OrderEnumConfig { + /// the enumeration name + pub type_name: crate::SimpleNamingFn, +} + +impl std::default::Default for OrderEnumConfig { + fn default() -> Self { + OrderEnumConfig { + type_name: Box::new(|object_name: &str| -> String { + format!("{object_name}NewOrderEnumInput") + }), + } + } +} + +/// The OrderByEnumeration is used for Entities Fields sorting +pub struct OrderEnumBuilder { + pub context: &'static BuilderContext, +} + +impl OrderEnumBuilder { + pub fn type_name(&self, object_name: &str) -> String { + self.context.order_enum.type_name.as_ref()(object_name) + } + + /// used to get the GraphQL enumeration config + pub fn enumeration(&self) -> Enum + where + T: EntityTrait, + ::Model: Sync, + { + let entity_object_builder = EntityObjectBuilder { + context: self.context, + }; + let object_name = entity_object_builder.type_name::(); + T::Column::iter().fold(Enum::new(self.type_name(&object_name)), |enu, column| { + enu.item(EnumItem::new( + entity_object_builder.column_name::(&column), + )) + }) + } +} \ No newline at end of file diff --git a/src/inputs/active_enum_filter_input.rs b/src/inputs/active_enum_filter_input.rs index c8060889..7ab32944 100644 --- a/src/inputs/active_enum_filter_input.rs +++ b/src/inputs/active_enum_filter_input.rs @@ -91,9 +91,15 @@ where let variant = variant.to_string(); let variant = if cfg!(feature = "field-snake-case") { variant.to_snake_case() + } else if cfg!(feature = "offset-pagination") { + variant + .chars() + .filter(|c| c.is_alphanumeric()) + .collect::() } else { variant.to_upper_camel_case().to_ascii_uppercase() }; + variant.eq(input) }); variant.unwrap().to_string() @@ -141,7 +147,7 @@ where condition }; - let condition = match filter.get("is_in") { + let condition = match filter.get("in") { Some(data) => { let data: Vec<_> = data .list() diff --git a/src/inputs/cascade_input.rs b/src/inputs/cascade_input.rs new file mode 100644 index 00000000..4371aa91 --- /dev/null +++ b/src/inputs/cascade_input.rs @@ -0,0 +1,132 @@ +use async_graphql::dynamic::{InputObject, InputValue, ObjectAccessor, TypeRef}; +use sea_orm::{EntityTrait, Iterable, JoinType, QuerySelect, RelationTrait}; + +use crate::{BuilderContext, CascadeBuilder, EntityObjectBuilder, FilterInputBuilder}; +use heck::ToUpperCamelCase; + +/// The configuration structure for FilterInputBuilder +pub struct CascadeInputConfig { + /// the filter input type name formatter function + pub type_name: crate::SimpleNamingFn, +} + +impl std::default::Default for CascadeInputConfig { + fn default() -> Self { + CascadeInputConfig { + type_name: Box::new(|object_name: &str| -> String { + format!("{object_name}CascadeInput") + }), + } + } +} + +/// This builder is used to produce the filter input object of a SeaORM entity +pub struct CascadeInputBuilder { + pub context: &'static BuilderContext, +} + +impl CascadeInputBuilder { + /// 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 { + self.context.cascade_input.type_name.as_ref()(object_name) + } + + /// used to produce the filter input object of a SeaORM entity + pub fn to_object(&self) -> InputObject + where + T: EntityTrait, + ::Model: Sync, + { + let entity_object_builder = EntityObjectBuilder { + context: self.context, + }; + let filter_input_builder = FilterInputBuilder { + context: self.context, + }; + + let entity_name = entity_object_builder.type_name::(); + let cascade_name = self.type_name(&entity_name); + + let object = + T::Relation::iter().fold(InputObject::new(&cascade_name), |object, related_table| { + let related_table_name = related_table.def().to_tbl; + let fk_name = related_table.def().fk_name; + let relation_name = if let Some(fk_name) = fk_name { + fk_name + } else { + "".to_string() + }; + if relation_name.is_empty() { + return object; + } + dbg!(&entity_name); + match related_table_name { + sea_orm::sea_query::TableRef::Table(iden) => { + let name = iden.to_string(); + object.field(InputValue::new( + relation_name, + TypeRef::named( + filter_input_builder.type_name(&name.to_upper_camel_case()), + ), + )) + } + sea_orm::sea_query::TableRef::SchemaTable(_, iden) => { + let name = iden.to_string(); + object.field(InputValue::new( + relation_name, + TypeRef::named( + filter_input_builder.type_name(&name.to_upper_camel_case()), + ), + )) + } + sea_orm::sea_query::TableRef::DatabaseSchemaTable(_, _, iden) => { + let name = iden.to_string().to_upper_camel_case(); + object.field(InputValue::new( + relation_name, + TypeRef::named( + filter_input_builder.type_name(&name.to_upper_camel_case()), + ), + )) + } + _ => object, + } + }); + + object + } + pub fn parse_object( + &self, + context: &'static BuilderContext, + cascades: Option>, + ) -> sea_orm::Select + where + T: EntityTrait, + ::Model: Sync, + ::Relation: CascadeBuilder, + { + if let Some(cascades) = cascades { + T::Relation::iter().fold(T::find(), |stmt, relation_definition| { + let context: &'static BuilderContext = context; + let fk_name = relation_definition.def().fk_name; + let relation_name = if let Some(fk_name) = fk_name { + fk_name + } else { + "".to_string() + }; + + if let Some(cascade) = cascades.get(&relation_name) { + stmt.join( + JoinType::InnerJoin, + CascadeBuilder::get_join(&relation_definition, &context, Some(cascade)), + ) + .distinct() + } else { + stmt + } + }) + } else { + T::find() + } + } +} \ No newline at end of file diff --git a/src/inputs/entity_input.rs b/src/inputs/entity_input.rs index 123dec7a..9981c2c9 100644 --- a/src/inputs/entity_input.rs +++ b/src/inputs/entity_input.rs @@ -100,9 +100,8 @@ impl EntityInputBuilder { Some(_) => T::PrimaryKey::auto_increment(), None => false, }; - let has_default_expr = column_def.get_column_default().is_some(); - let is_insert_not_nullable = - is_insert && !(column_def.is_null() || auto_increment || has_default_expr); + //let has_default_expr = column_def.get_column_default().is_some(); + let is_insert_not_nullable = is_insert && !(column_def.is_null() || auto_increment); let graphql_type = match types_map_helper.sea_orm_column_type_to_graphql_type( column_def.get_column_type(), @@ -167,4 +166,4 @@ impl EntityInputBuilder { Ok(map) } -} +} \ No newline at end of file diff --git a/src/inputs/filter_input.rs b/src/inputs/filter_input.rs index d591e3c3..5db75daa 100644 --- a/src/inputs/filter_input.rs +++ b/src/inputs/filter_input.rs @@ -57,5 +57,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))) } -} +} \ No newline at end of file diff --git a/src/inputs/mod.rs b/src/inputs/mod.rs index 5b83c6c2..a0087761 100644 --- a/src/inputs/mod.rs +++ b/src/inputs/mod.rs @@ -22,3 +22,10 @@ pub use entity_input::*; pub mod active_enum_filter_input; pub use active_enum_filter_input::*; + +pub mod new_order_input; +pub use new_order_input::*; + + +pub mod cascade_input; +pub use cascade_input::*; \ No newline at end of file diff --git a/src/inputs/new_order_input.rs b/src/inputs/new_order_input.rs new file mode 100644 index 00000000..84f5543b --- /dev/null +++ b/src/inputs/new_order_input.rs @@ -0,0 +1,91 @@ +use async_graphql::dynamic::{InputObject, InputValue, TypeRef, ValueAccessor}; +use sea_orm::{EntityTrait, Iterable}; + +use crate::{BuilderContext, EntityObjectBuilder}; + +/// The configuration structure for OrderInputBuilder +pub struct NewOrderInputConfig { + /// used to format OrderInput object name + pub type_name: crate::SimpleNamingFn, +} + +impl std::default::Default for NewOrderInputConfig { + fn default() -> Self { + NewOrderInputConfig { + type_name: Box::new(|object_name: &str| -> String { + format!("{object_name}NewOrderInput") + }), + } + } +} + +/// This builder produces the OrderInput object of a SeaORM entity +pub struct NewOrderInputBuilder { + pub context: &'static BuilderContext, +} + +impl NewOrderInputBuilder { + /// used to get type name + pub fn type_name(&self, object_name: &str) -> String { + self.context.new_order_input.type_name.as_ref()(object_name) + } + + /// used to get the OrderInput object of a SeaORM entity + pub fn to_object(&self) -> InputObject + where + T: EntityTrait, + ::Model: Sync, + { + let entity_object_builder = EntityObjectBuilder { + context: self.context, + }; + + let object_name = entity_object_builder.type_name::(); + let name = self.type_name(&object_name); + + InputObject::new(name) + .field(InputValue::new( + "asc", + TypeRef::named(&(self.context.order_enum.type_name)(&object_name)), + )) + .field(InputValue::new( + "desc", + TypeRef::named(&(self.context.order_enum.type_name)(&object_name)), + )) + } + + pub fn parse_object( + &self, + value: Option>, + ) -> Vec<(T::Column, sea_orm::sea_query::Order)> + where + T: EntityTrait, + ::Model: Sync, + { + match value { + Some(value) => { + let mut data = Vec::new(); + let order = value.object().unwrap(); + let entity_object = EntityObjectBuilder { + context: self.context, + }; + for col in T::Column::iter() { + let column_name = entity_object.column_name::(&col); + if let Some(order) = order.get("asc") { + if column_name == order.enum_name().unwrap() { + data.push((col, sea_orm::Order::Asc)); + } + } + + if let Some(order) = order.get("desc") { + if column_name == order.enum_name().unwrap() { + data.push((col, sea_orm::Order::Desc)); + } + } + } + data + } + None => Vec::new(), + } + } +} \ No newline at end of file diff --git a/src/outputs/connection_object.rs b/src/outputs/connection_object.rs index 87b77e35..22b641ad 100644 --- a/src/outputs/connection_object.rs +++ b/src/outputs/connection_object.rs @@ -110,7 +110,7 @@ impl ConnectionObjectBuilder { if let Some(value) = connection .pagination_info .as_ref() - .map(FieldValue::borrowed_any) + .map(|info| FieldValue::borrowed_any(info)) { Ok(Some(value)) } else { @@ -147,4 +147,4 @@ impl ConnectionObjectBuilder { }, )) } -} +} \ No newline at end of file diff --git a/src/outputs/entity_object.rs b/src/outputs/entity_object.rs index f4c83f86..662e8d18 100644 --- a/src/outputs/entity_object.rs +++ b/src/outputs/entity_object.rs @@ -212,9 +212,20 @@ fn sea_query_value_to_graphql_value( sea_orm::Value::BigUnsigned(value) => value.map(Value::from), sea_orm::Value::Float(value) => value.map(Value::from), sea_orm::Value::Double(value) => value.map(Value::from), - sea_orm::Value::String(value) if is_enum => { - value.map(|it| Value::from(it.as_str().to_upper_camel_case().to_ascii_uppercase())) - } + sea_orm::Value::String(value) if is_enum => value.map(|it| { + if cfg!(feature = "field-snake-case") { + Value::from(it.as_str().to_snake_case()) + } else if cfg!(feature = "offset-pagination") { + Value::from( + it.chars() + .filter(|c| c.is_alphanumeric()) + .collect::() + .as_str(), + ) + } else { + Value::from(it.as_str().to_upper_camel_case().to_ascii_uppercase()) + } + }), sea_orm::Value::String(value) => value.map(|it| Value::from(it.as_str())), sea_orm::Value::Char(value) => value.map(|it| Value::from(it.to_string())), @@ -309,4 +320,4 @@ fn sea_query_value_to_graphql_value( #[allow(unreachable_patterns)] _ => panic!("Cannot convert SeaORM value"), } -} +} \ No newline at end of file diff --git a/src/query/cascading.rs b/src/query/cascading.rs new file mode 100644 index 00000000..d698abcb --- /dev/null +++ b/src/query/cascading.rs @@ -0,0 +1,9 @@ +use async_graphql::dynamic::{ObjectAccessor, ValueAccessor}; + +pub fn get_cascade_conditions(cascades: Option) -> Option { + if let Some(cascades) = cascades { + Some(cascades.object().unwrap()) + } else { + None + } +} diff --git a/src/query/entity_get_field.rs b/src/query/entity_get_field.rs new file mode 100644 index 00000000..cc860efa --- /dev/null +++ b/src/query/entity_get_field.rs @@ -0,0 +1,123 @@ +use async_graphql::{ + dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}, + Error, +}; +use heck::ToLowerCamelCase; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, Iterable, QueryFilter}; + +use crate::{BuilderContext, EntityObjectBuilder, GuardAction, TypesMapHelper}; + +/// The configuration structure for EntityQueryFieldBuilder +pub struct EntityGetFieldConfig { + /// used to format entity field name + pub type_name: crate::SimpleNamingFn, +} + +impl std::default::Default for EntityGetFieldConfig { + fn default() -> Self { + EntityGetFieldConfig { + type_name: Box::new(|object_name: &str| -> String { + ("get".to_owned() + object_name).to_lower_camel_case() + }), + } + } +} + +/// This builder produces a field for the Query object that queries a SeaORM entity +pub struct EntityGetFieldBuilder { + pub context: &'static BuilderContext, +} + +impl EntityGetFieldBuilder { + /// used to get field name for a SeaORM entity + pub fn type_name(&self) -> String + where + T: EntityTrait, + ::Model: Sync, + { + let entity_object = EntityObjectBuilder { + context: self.context, + }; + let object_name = entity_object.type_name::(); + self.context.entity_get_field.type_name.as_ref()(&object_name) + } + + /// used to get the Query object field for a SeaORM entity + pub fn to_field(&self) -> Field + where + T: EntityTrait, + ::Model: Sync, + { + let entity_object = EntityObjectBuilder { + context: self.context, + }; + + let object_name = entity_object.type_name::(); + let type_ref = TypeRef::named(&object_name); + let resolver_fn = |object: T::Model| FieldValue::owned_any(object); + let guard = self.context.guards.entity_guards.get(&object_name); + + let context: &'static BuilderContext = self.context; + + let field = Field::new(self.type_name::(), type_ref, { + move |ctx| { + let context: &'static BuilderContext = context; + FieldFuture::new({ + async move { + let guard_flag = if let Some(guard) = guard { + (*guard)(&ctx) + } else { + GuardAction::Allow + }; + + if let GuardAction::Block(reason) = guard_flag { + return match reason { + Some(reason) => { + Err::, async_graphql::Error>(Error::new(reason)) + } + None => Err::, async_graphql::Error>(Error::new( + "Entity guard triggered.", + )), + }; + } + + let stmt = T::find(); + let stmt = T::Column::iter().fold(stmt, |stmt, column| { + let entity_object_builder = EntityObjectBuilder { context }; + let column_name = entity_object_builder.column_name::(&column); + let types_map_helper = TypesMapHelper { context }; + match ctx.args.get(&column_name) { + Some(val) => stmt.filter( + column.eq(types_map_helper + .async_graphql_value_to_sea_orm_value::(&column, &val) + .unwrap()), + ), + _ => stmt, + } + }); + + let db = ctx.data::()?; + + let object = stmt.one(db).await?; + + match object { + Some(object) => Ok(Some(resolver_fn(object))), + _ => Ok(Some(FieldValue::NULL)), + } + } + }) + } + }); + + T::Column::iter().fold(field, |field, column| { + let column_name = entity_object.column_name::(&column); + let types_map_helper = TypesMapHelper { context }; + field.argument(InputValue::new( + column_name, + types_map_helper + .sea_orm_column_type_to_graphql_type(column.def().get_column_type(), false) + .unwrap(), + )) + }) + } +} \ No newline at end of file diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index 37152a5a..c081c461 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -1,15 +1,17 @@ use async_graphql::{ dataloader::DataLoader, - dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}, + dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef, ValueAccessor}, Error, }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{EntityTrait, Iden, ModelTrait, RelationDef}; +#[cfg(not(feature = "offset-pagination"))] +use crate::ConnectionObjectBuilder; use crate::{ - apply_memory_pagination, get_filter_conditions, BuilderContext, Connection, - ConnectionObjectBuilder, EntityObjectBuilder, FilterInputBuilder, GuardAction, - HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, OrderInputBuilder, + apply_memory_pagination, get_filter_conditions, BuilderContext, EntityObjectBuilder, + FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, NewOrderInputBuilder, + OffsetInput, OneToManyLoader, OneToOneLoader, OrderInputBuilder, PageInput, PaginationInput, PaginationInputBuilder, }; @@ -37,11 +39,23 @@ impl EntityObjectRelationBuilder { }; let context: &'static BuilderContext = self.context; let entity_object_builder = EntityObjectBuilder { context }; + #[cfg(not(feature = "offset-pagination"))] let connection_object_builder = ConnectionObjectBuilder { context }; let filter_input_builder = FilterInputBuilder { context }; let order_input_builder = OrderInputBuilder { context }; + let new_order_input_builder = NewOrderInputBuilder { context }; let object_name: String = entity_object_builder.type_name::(); + #[cfg(feature = "offset-pagination")] + let type_ref = TypeRef::named_list(&object_name); + #[cfg(not(feature = "offset-pagination"))] + let type_ref = TypeRef::named_nn(connection_object_builder.type_name(&object_name)); + + #[cfg(feature = "offset-pagination")] + let resolver_fn = + |object: Vec| FieldValue::list(object.into_iter().map(FieldValue::owned_any)); + #[cfg(not(feature = "offset-pagination"))] + let resolver_fn = |object: crate::Connection| FieldValue::owned_any(object); let guard = self.context.guards.entity_guards.get(&object_name); let from_col = ::from_str( @@ -93,7 +107,11 @@ impl EntityObjectRelationBuilder { 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 mut order_by = OrderInputBuilder { context }.parse_object::(order_by); + let order = ctx.args.get(&context.entity_query_field.order); + let order = NewOrderInputBuilder { context }.parse_object::(order); + order_by.extend(order); + let key = KeyComplex:: { key: vec![parent.get(from_col)], meta: HashableGroupKey:: { @@ -113,80 +131,133 @@ impl EntityObjectRelationBuilder { } }) }), - true => Field::new( - name, - TypeRef::named_nn(connection_object_builder.type_name(&object_name)), - move |ctx| { - let context: &'static BuilderContext = context; - FieldFuture::new(async move { - let guard_flag = if let Some(guard) = guard { - (*guard)(&ctx) - } else { - GuardAction::Allow - }; + true => Field::new(name, type_ref, move |ctx| { + let context: &'static BuilderContext = context; + FieldFuture::new(async move { + let guard_flag = if let Some(guard) = guard { + (*guard)(&ctx) + } else { + GuardAction::Allow + }; - if let GuardAction::Block(reason) = guard_flag { - return match reason { - Some(reason) => { - Err::, async_graphql::Error>(Error::new(reason)) - } - None => Err::, async_graphql::Error>(Error::new( - "Entity guard triggered.", - )), - }; - } - - let parent: &T::Model = ctx - .parent_value - .try_downcast_ref::() - .expect("Parent should exist"); - - let loader = ctx.data_unchecked::>>(); - - let stmt = R::find(); - 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)], - meta: HashableGroupKey:: { - stmt, - columns: vec![to_col], - filters: Some(filters), - order_by, - }, + if let GuardAction::Block(reason) = guard_flag { + return match reason { + Some(reason) => { + Err::, async_graphql::Error>(Error::new(reason)) + } + None => Err::, async_graphql::Error>(Error::new( + "Entity guard triggered.", + )), }; + } - let values = loader.load_one(key).await?; + let parent: &T::Model = ctx + .parent_value + .try_downcast_ref::() + .expect("Parent should exist"); + + let loader = ctx.data_unchecked::>>(); + + let stmt = R::find(); + 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 mut order_by = OrderInputBuilder { context }.parse_object::(order_by); + let order = ctx.args.get(&context.entity_query_field.order); + let order = NewOrderInputBuilder { context }.parse_object::(order); + order_by.extend(order); + let key = KeyComplex:: { + key: vec![parent.get(from_col)], + meta: HashableGroupKey:: { + stmt, + columns: vec![to_col], + filters: Some(filters), + order_by, + }, + }; - let pagination = ctx.args.get(&context.entity_query_field.pagination); - let pagination = - PaginationInputBuilder { context }.parse_object(pagination); + let values = loader.load_one(key).await?; + let pagination = ctx.args.get(&context.entity_query_field.pagination); + let pagination = PaginationInputBuilder { context }.parse_object(pagination); + let first = ctx.args.get("first"); + let pagination = match first { + Some(first_value) => match first_value.u64() { + Ok(first_num) => { + if let Some(offset) = pagination.offset { + PaginationInput { + offset: Some(OffsetInput { + offset: offset.offset, + limit: first_num, + }), + page: None, + cursor: None, + } + } else if let Some(page) = pagination.page { + PaginationInput { + offset: None, + page: Some(PageInput { + page: page.page, + limit: first_num, + }), + cursor: None, + } + } else { + PaginationInput { + offset: Some(OffsetInput { + offset: 0, + limit: first_num, + }), + page: None, + cursor: None, + } + } + } + _error => pagination, + }, + None => pagination, + }; - let connection: Connection = apply_memory_pagination(values, pagination); + let object = apply_memory_pagination::(values, pagination); - Ok(Some(FieldValue::owned_any(connection))) - }) - }, - ), + Ok(Some(resolver_fn(object))) + }) + }), }; - match relation_definition.is_owner { - false => field, - true => field - .argument(InputValue::new( - &context.entity_query_field.filters, - TypeRef::named(filter_input_builder.type_name(&object_name)), - )) - .argument(InputValue::new( - &context.entity_query_field.order_by, - TypeRef::named(order_input_builder.type_name(&object_name)), - )) - .argument(InputValue::new( - &context.entity_query_field.pagination, - TypeRef::named(&context.pagination_input.type_name), - )), - } + field + .argument(InputValue::new( + &context.entity_query_field.filters, + TypeRef::named(filter_input_builder.type_name(&object_name)), + )) + .argument(InputValue::new( + &context.entity_query_field.order_by, + TypeRef::named(order_input_builder.type_name(&object_name)), + )) + .argument(InputValue::new( + &self.context.entity_query_field.order, + TypeRef::named(new_order_input_builder.type_name(&object_name)), + )) + .argument(InputValue::new( + &context.entity_query_field.pagination, + TypeRef::named(&context.pagination_input.type_name), + )) + .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) + } + + pub fn joiin( + &self, + relation_definition: RelationDef, + filter: Option, + ) -> RelationDef + where + T: EntityTrait, + ::Model: Sync, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + R: EntityTrait, + ::Model: Sync, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + { + let filters = get_filter_conditions::(self.context, filter); + relation_definition.on_condition(move |_left, _right| filters.to_owned()) } } diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index 1be03fa6..60fe62b6 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -1,18 +1,21 @@ use async_graphql::{ dataloader::DataLoader, - dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}, + dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef, ValueAccessor}, Error, }; use heck::{ToLowerCamelCase, ToSnakeCase}; use sea_orm::{ - ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, Related, + ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, + Related, RelationDef, }; +#[cfg(not(feature = "offset-pagination"))] +use crate::ConnectionObjectBuilder; use crate::{ apply_memory_pagination, apply_order, apply_pagination, get_filter_conditions, BuilderContext, - ConnectionObjectBuilder, EntityObjectBuilder, FilterInputBuilder, GuardAction, - HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, OrderInputBuilder, - PaginationInputBuilder, + EntityObjectBuilder, FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, + NewOrderInputBuilder, OffsetInput, OneToManyLoader, OneToOneLoader, OrderInputBuilder, + PageInput, PaginationInput, PaginationInputBuilder, }; /// This builder produces a GraphQL field for an SeaORM entity related trait @@ -46,11 +49,22 @@ impl EntityObjectViaRelationBuilder { }; let entity_object_builder = EntityObjectBuilder { context }; + #[cfg(not(feature = "offset-pagination"))] let connection_object_builder = ConnectionObjectBuilder { context }; let filter_input_builder = FilterInputBuilder { context }; let order_input_builder = OrderInputBuilder { context }; - + let new_order_input_builder = NewOrderInputBuilder { context }; let object_name: String = entity_object_builder.type_name::(); + #[cfg(feature = "offset-pagination")] + let type_ref = TypeRef::named_list(&object_name); + #[cfg(not(feature = "offset-pagination"))] + let type_ref = TypeRef::named_nn(connection_object_builder.type_name(&object_name)); + + #[cfg(feature = "offset-pagination")] + let resolver_fn = + |object: Vec| FieldValue::list(object.into_iter().map(FieldValue::owned_any)); + #[cfg(not(feature = "offset-pagination"))] + let resolver_fn = |object: crate::Connection| FieldValue::owned_any(object); let guard = self.context.guards.entity_guards.get(&object_name); let from_col = ::from_str( @@ -107,7 +121,10 @@ impl EntityObjectViaRelationBuilder { 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 mut order_by = OrderInputBuilder { context }.parse_object::(order_by); + let order = ctx.args.get(&context.entity_query_field.order); + let order = NewOrderInputBuilder { context }.parse_object::(order); + order_by.extend(order); let key = KeyComplex:: { key: vec![parent.get(from_col)], meta: HashableGroupKey:: { @@ -127,100 +144,153 @@ impl EntityObjectViaRelationBuilder { } }) }), - true => Field::new( - name, - TypeRef::named_nn(connection_object_builder.type_name(&object_name)), - move |ctx| { - let context: &'static BuilderContext = context; - FieldFuture::new(async move { - let guard_flag = if let Some(guard) = guard { - (*guard)(&ctx) - } else { - GuardAction::Allow - }; + true => Field::new(name, type_ref, move |ctx| { + let context: &'static BuilderContext = context; + FieldFuture::new(async move { + let guard_flag = if let Some(guard) = guard { + (*guard)(&ctx) + } else { + GuardAction::Allow + }; - if let GuardAction::Block(reason) = guard_flag { - return match reason { - Some(reason) => { - Err::, async_graphql::Error>(Error::new(reason)) - } - None => Err::, async_graphql::Error>(Error::new( - "Entity guard triggered.", - )), - }; - } - - // FIXME: optimize union queries - // NOTE: each has unique query in order to apply pagination... - let parent: &T::Model = ctx - .parent_value - .try_downcast_ref::() - .expect("Parent should exist"); - - let stmt = if >::via().is_some() { - >::find_related() - } else { - R::find() + if let GuardAction::Block(reason) = guard_flag { + return match reason { + Some(reason) => { + Err::, async_graphql::Error>(Error::new(reason)) + } + None => Err::, async_graphql::Error>(Error::new( + "Entity guard triggered.", + )), }; + } - let filters = ctx.args.get(&context.entity_query_field.filters); - let filters = get_filter_conditions::(context, filters); + // FIXME: optimize union queries + // NOTE: each has unique query in order to apply pagination... + let parent: &T::Model = ctx + .parent_value + .try_downcast_ref::() + .expect("Parent should exist"); - let order_by = ctx.args.get(&context.entity_query_field.order_by); - let order_by = OrderInputBuilder { context }.parse_object::(order_by); + let stmt = if >::via().is_some() { + >::find_related() + } else { + R::find() + }; - let pagination = ctx.args.get(&context.entity_query_field.pagination); - let pagination = - PaginationInputBuilder { context }.parse_object(pagination); + let filters = ctx.args.get(&context.entity_query_field.filters); + let filters = get_filter_conditions::(context, filters); - let db = ctx.data::()?; + let order_by = ctx.args.get(&context.entity_query_field.order_by); + let mut order_by = OrderInputBuilder { context }.parse_object::(order_by); - let connection = if is_via_relation { - // TODO optimize query - let condition = Condition::all().add(from_col.eq(parent.get(from_col))); + let order = ctx.args.get(&context.entity_query_field.order); + let order = NewOrderInputBuilder { context }.parse_object::(order); + order_by.extend(order); - let stmt = stmt.filter(condition.add(filters)); - let stmt = apply_order(stmt, order_by); - apply_pagination::(db, stmt, pagination).await? - } else { - let loader = ctx.data_unchecked::>>(); + let pagination = ctx.args.get(&context.entity_query_field.pagination); + let pagination = PaginationInputBuilder { context }.parse_object(pagination); + let first = ctx.args.get("first"); + let pagination = match first { + Some(first_value) => match first_value.u64() { + Ok(first_num) => { + if let Some(offset) = pagination.offset { + PaginationInput { + offset: Some(OffsetInput { + offset: offset.offset, + limit: first_num, + }), + page: None, + cursor: None, + } + } else if let Some(page) = pagination.page { + PaginationInput { + offset: None, + page: Some(PageInput { + page: page.page, + limit: first_num, + }), + cursor: None, + } + } else { + PaginationInput { + offset: Some(OffsetInput { + offset: 0, + limit: first_num, + }), + page: None, + cursor: None, + } + } + } + _error => pagination, + }, + None => pagination, + }; + let db = ctx.data::()?; - let key = KeyComplex:: { - key: vec![parent.get(from_col)], - meta: HashableGroupKey:: { - stmt, - columns: vec![to_col], - filters: Some(filters), - order_by, - }, - }; + let object = if is_via_relation { + // TODO optimize query + let condition = Condition::all().add(from_col.eq(parent.get(from_col))); - let values = loader.load_one(key).await?; + let stmt = stmt.filter(condition.add(filters)); + let stmt = apply_order(stmt, order_by); + apply_pagination::(db, stmt, pagination).await? + } else { + let loader = ctx.data_unchecked::>>(); - apply_memory_pagination(values, pagination) + let key = KeyComplex:: { + key: vec![parent.get(from_col)], + meta: HashableGroupKey:: { + stmt, + columns: vec![to_col], + filters: Some(filters), + order_by, + }, }; - Ok(Some(FieldValue::owned_any(connection))) - }) - }, - ), + let values = loader.load_one(key).await?; + apply_memory_pagination::(values, pagination) + }; + + Ok(Some(resolver_fn(object))) + }) + }), }; - match via_relation_definition.is_owner { - false => field, - true => field - .argument(InputValue::new( - &context.entity_query_field.filters, - TypeRef::named(filter_input_builder.type_name(&object_name)), - )) - .argument(InputValue::new( - &context.entity_query_field.order_by, - TypeRef::named(order_input_builder.type_name(&object_name)), - )) - .argument(InputValue::new( - &context.entity_query_field.pagination, - TypeRef::named(&context.pagination_input.type_name), - )), - } + field + .argument(InputValue::new( + &context.entity_query_field.filters, + TypeRef::named(filter_input_builder.type_name(&object_name)), + )) + .argument(InputValue::new( + &context.entity_query_field.order_by, + TypeRef::named(order_input_builder.type_name(&object_name)), + )) + .argument(InputValue::new( + &self.context.entity_query_field.order, + TypeRef::named(new_order_input_builder.type_name(&object_name)), + )) + .argument(InputValue::new( + &context.entity_query_field.pagination, + TypeRef::named(&context.pagination_input.type_name), + )) + .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) } -} + + pub fn joiin( + &self, + relation_definition: RelationDef, + filter: Option, + ) -> RelationDef + where + T: EntityTrait, + ::Model: Sync, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + R: EntityTrait, + ::Model: Sync, + <::Column as std::str::FromStr>::Err: core::fmt::Debug, + { + let filters = get_filter_conditions::(self.context, filter); + relation_definition.on_condition(move |_left, _right| filters.to_owned()) + } +} \ No newline at end of file diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index ae0ef7a2..26bc9d4c 100644 --- a/src/query/entity_query_field.rs +++ b/src/query/entity_query_field.rs @@ -3,14 +3,20 @@ use async_graphql::{ Error, }; use heck::{ToLowerCamelCase, ToSnakeCase}; -use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter}; +use sea_orm::{ + DatabaseConnection, EntityTrait, QueryFilter +}; +#[cfg(not(feature = "offset-pagination"))] +use crate::ConnectionObjectBuilder; use crate::{ - apply_order, apply_pagination, get_filter_conditions, BuilderContext, ConnectionObjectBuilder, - EntityObjectBuilder, FilterInputBuilder, GuardAction, OrderInputBuilder, - PaginationInputBuilder, + apply_order, apply_pagination, get_filter_conditions, get_first, BuilderContext, + CascadeInputBuilder, EntityObjectBuilder, FilterInputBuilder, GuardAction, + NewOrderInputBuilder, OrderInputBuilder, PaginationInputBuilder, }; +use super::get_cascade_conditions; + /// The configuration structure for EntityQueryFieldBuilder pub struct EntityQueryFieldConfig { /// used to format entity field name @@ -21,6 +27,7 @@ pub struct EntityQueryFieldConfig { pub order_by: String, /// name for 'pagination' field pub pagination: String, + pub order: String, } impl std::default::Default for EntityQueryFieldConfig { @@ -33,7 +40,7 @@ impl std::default::Default for EntityQueryFieldConfig { object_name.to_lower_camel_case() } }), - filters: "filters".into(), + filters: "filter".into(), order_by: { if cfg!(feature = "field-snake-case") { "order_by" @@ -43,6 +50,7 @@ impl std::default::Default for EntityQueryFieldConfig { .into() }, pagination: "pagination".into(), + order: "order".into(), } } } @@ -72,70 +80,100 @@ impl EntityQueryFieldBuilder { T: EntityTrait, ::Model: Sync, { + #[cfg(not(feature = "offset-pagination"))] let connection_object_builder = ConnectionObjectBuilder { context: self.context, }; let filter_input_builder = FilterInputBuilder { context: self.context, }; + let cascade_input_builder = CascadeInputBuilder { + context: self.context, + }; let order_input_builder = OrderInputBuilder { context: self.context, }; let pagination_input_builder = PaginationInputBuilder { context: self.context, }; + let new_order_input_builder = NewOrderInputBuilder { + context: self.context, + }; let entity_object = EntityObjectBuilder { context: self.context, }; let object_name = entity_object.type_name::(); - let type_name = connection_object_builder.type_name(&object_name); - + #[cfg(feature = "offset-pagination")] + let type_ref = TypeRef::named_list(&object_name); + #[cfg(not(feature = "offset-pagination"))] + let type_ref = TypeRef::named_nn(connection_object_builder.type_name(&object_name)); + #[cfg(feature = "offset-pagination")] + let resolver_fn = + |object: Vec| FieldValue::list(object.into_iter().map(FieldValue::owned_any)); + #[cfg(not(feature = "offset-pagination"))] + let resolver_fn = |object: crate::Connection| FieldValue::owned_any(object); let guard = self.context.guards.entity_guards.get(&object_name); let context: &'static BuilderContext = self.context; - Field::new( - self.type_name::(), - TypeRef::named_nn(type_name), + + Field::new(self.type_name::(), type_ref, { move |ctx| { let context: &'static BuilderContext = context; - FieldFuture::new(async move { - let guard_flag = if let Some(guard) = guard { - (*guard)(&ctx) - } else { - GuardAction::Allow - }; - - if let GuardAction::Block(reason) = guard_flag { - return match reason { - Some(reason) => { - Err::, async_graphql::Error>(Error::new(reason)) - } - None => Err::, async_graphql::Error>(Error::new( - "Entity guard triggered.", - )), + FieldFuture::new({ + async move { + let guard_flag = if let Some(guard) = guard { + (*guard)(&ctx) + } else { + GuardAction::Allow }; - } - 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 pagination = ctx.args.get(&context.entity_query_field.pagination); - let pagination = PaginationInputBuilder { context }.parse_object(pagination); + if let GuardAction::Block(reason) = guard_flag { + return match reason { + Some(reason) => { + Err::, async_graphql::Error>(Error::new(reason)) + } + None => Err::, async_graphql::Error>(Error::new( + "Entity guard triggered.", + )), + }; + } - let stmt = T::find(); - let stmt = stmt.filter(filters); - let stmt = apply_order(stmt, order_by); + 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 mut order_by = + OrderInputBuilder { context }.parse_object::(order_by); + let order = ctx.args.get(&context.entity_query_field.order); + let order = NewOrderInputBuilder { context }.parse_object::(order); + order_by.extend(order); + let pagination = ctx.args.get(&context.entity_query_field.pagination); + let first = ctx.args.get("first"); + let pagination = + PaginationInputBuilder { context }.parse_object(pagination); + let pagination = get_first(first, pagination); + let _cascades = ctx.args.get("cascade"); + let _cascades = get_cascade_conditions(_cascades); - let db = ctx.data::()?; + //let stmt = + // CascadeInputBuilder { context }.parse_object::(context, cascades); + let stmt = T::find(); + let stmt = stmt.filter(filters); + let stmt = apply_order(stmt, order_by); - let connection = apply_pagination::(db, stmt, pagination).await?; + let db = ctx.data::()?; - Ok(Some(FieldValue::owned_any(connection))) + let object = apply_pagination(db, stmt, pagination).await?; + + Ok(Some(resolver_fn(object))) + } }) - }, - ) + } + }) + .argument(InputValue::new( + "cascade", + TypeRef::named(cascade_input_builder.type_name(&object_name)), + )) .argument(InputValue::new( &self.context.entity_query_field.filters, TypeRef::named(filter_input_builder.type_name(&object_name)), @@ -144,9 +182,14 @@ impl EntityQueryFieldBuilder { &self.context.entity_query_field.order_by, TypeRef::named(order_input_builder.type_name(&object_name)), )) + .argument(InputValue::new( + &self.context.entity_query_field.order, + TypeRef::named(new_order_input_builder.type_name(&object_name)), + )) .argument(InputValue::new( &self.context.entity_query_field.pagination, TypeRef::named(pagination_input_builder.type_name()), )) + .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) } -} +} \ No newline at end of file diff --git a/src/query/filtering.rs b/src/query/filtering.rs index 8a27a45b..634c7f0e 100644 --- a/src/query/filtering.rs +++ b/src/query/filtering.rs @@ -80,5 +80,16 @@ where condition }; + let condition = if let Some(not) = filters.get("not") { + let filter = not.object().unwrap(); + condition.add( + Condition::all() + .add(recursive_prepare_condition::(context, filter)) + .not(), + ) + } else { + condition + }; + condition -} +} \ No newline at end of file diff --git a/src/query/first.rs b/src/query/first.rs new file mode 100644 index 00000000..51cde060 --- /dev/null +++ b/src/query/first.rs @@ -0,0 +1,41 @@ +use crate::{OffsetInput, PageInput, PaginationInput}; +use async_graphql::dynamic::ValueAccessor; + +pub fn get_first(first: Option, pagination: PaginationInput) -> PaginationInput { + match first { + Some(first_value) => match first_value.u64() { + Ok(first_num) => { + if let Some(offset) = pagination.offset { + PaginationInput { + offset: Some(OffsetInput { + offset: offset.offset, + limit: first_num, + }), + page: None, + cursor: None, + } + } else if let Some(page) = pagination.page { + PaginationInput { + offset: None, + page: Some(PageInput { + page: page.page, + limit: first_num, + }), + cursor: None, + } + } else { + PaginationInput { + offset: Some(OffsetInput { + offset: 0, + limit: first_num, + }), + page: None, + cursor: None, + } + } + } + _error => pagination, + }, + None => pagination, + } +} \ No newline at end of file diff --git a/src/query/mod.rs b/src/query/mod.rs index 4c9f9f15..4dc77f85 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -18,3 +18,13 @@ pub use entity_object_relation::*; pub mod entity_object_via_relation; pub use entity_object_via_relation::*; + +pub mod cascading; +pub use cascading::*; + +pub mod entity_get_field; +pub use entity_get_field::*; + + +pub mod first; +pub use first::*; \ No newline at end of file diff --git a/src/query/pagination.rs b/src/query/pagination.rs index e2c854e3..48212401 100644 --- a/src/query/pagination.rs +++ b/src/query/pagination.rs @@ -1,17 +1,20 @@ +#[allow(unused_imports)] +use crate::{ + decode_cursor, encode_cursor, map_cursor_values, Connection, Edge, PageInfo, PaginationInfo, + PaginationInput, +}; +#[cfg(not(feature = "offset-pagination"))] use itertools::Itertools; #[allow(unused_imports)] use sea_orm::CursorTrait; +#[allow(unused_imports)] use sea_orm::{ ConnectionTrait, DatabaseConnection, EntityTrait, Iterable, ModelTrait, PaginatorTrait, PrimaryKeyToColumn, QuerySelect, QueryTrait, Select, }; -use crate::{ - decode_cursor, encode_cursor, map_cursor_values, Connection, Edge, PageInfo, PaginationInfo, - PaginationInput, -}; - /// used to parse pagination input object and apply it to statement +#[cfg(not(feature = "offset-pagination"))] pub async fn apply_pagination( db: &DatabaseConnection, stmt: Select, @@ -263,6 +266,31 @@ where } } +#[cfg(feature = "offset-pagination")] +pub async fn apply_pagination( + db: &DatabaseConnection, + stmt: Select, + pagination: PaginationInput, +) -> Result::Model>, sea_orm::error::DbErr> +where + T: EntityTrait, + ::Model: Sync, +{ + if let Some(page_object) = pagination.page { + let paginator = stmt.paginate(db, page_object.limit); + + Ok(paginator.fetch_page(page_object.page).await?) + } else if let Some(offset_object) = pagination.offset { + let offset = offset_object.offset; + let limit = offset_object.limit; + + Ok(stmt.offset(offset).limit(limit).all(db).await?) + } else { + Ok(stmt.all(db).await?) + } +} + +#[cfg(not(feature = "offset-pagination"))] pub fn apply_memory_pagination( values: Option>, pagination: PaginationInput, @@ -411,3 +439,29 @@ where } } } + +#[cfg(feature = "offset-pagination")] +pub fn apply_memory_pagination( + values: Option>, + pagination: PaginationInput, +) -> Vec +where + T: EntityTrait, + T::Model: Sync, +{ + let data: Vec<::Model> = values.unwrap_or_default(); + + if let Some(page_object) = pagination.page { + data.into_iter() + .skip((page_object.page * page_object.limit).try_into().unwrap()) + .take(page_object.limit.try_into().unwrap()) + .collect() + } else if let Some(offset_object) = pagination.offset { + data.into_iter() + .skip((offset_object.offset).try_into().unwrap()) + .take(offset_object.limit.try_into().unwrap()) + .collect() + } else { + data + } +} \ No newline at end of file