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 782c24e1..918d48a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ categories = ["database"] [dependencies] async-graphql = { version = "7.0", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } -sea-orm = { version = "~1.1.0-rc.1", default-features = false, features = ["seaography"] } +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,5 +32,6 @@ 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"] +# with-mac_address = ["sea-orm/with-mac_address"] \ No newline at end of file diff --git a/examples/mysql/Cargo.toml b/examples/mysql/Cargo.toml index ab2acc05..bc90cfeb 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 b79ac2ca..ffb254a2 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 f75d44ea..99897494 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 9ee2914d..48ab0e61 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 59529b0b..909d154f 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 ec21acd7..624e4d86 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 2de75389..44dea5fc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -83,17 +83,23 @@ impl Builder { |entity_object, field| entity_object.field(field), ); - let edge_object_builder = EdgeObjectBuilder { - context: self.context, - }; - let edge = edge_object_builder.to_object::(); + if cfg!(feature = "offset-pagination") { + self.outputs.extend(vec![entity_object]); + } else { + let edge_object_builder = EdgeObjectBuilder { + context: self.context, + }; - let connection_object_builder = ConnectionObjectBuilder { - context: self.context, - }; - let connection = connection_object_builder.to_object::(); + let edge = edge_object_builder.to_object::(); - self.outputs.extend(vec![entity_object, edge, connection]); + let connection_object_builder = ConnectionObjectBuilder { + context: self.context, + }; + + let connection = connection_object_builder.to_object::(); + + self.outputs.extend(vec![entity_object, edge, connection]); + } let filter_input_builder = FilterInputBuilder { context: self.context, @@ -271,12 +277,8 @@ impl Builder { } .enumeration(), ) - .register( - CursorInputBuilder { - context: self.context, - } - .input_object(), - ) + .register(query) + .register(mutation) .register( CursorInputBuilder { context: self.context, @@ -313,8 +315,6 @@ impl Builder { } .to_object(), ) - .register(query) - .register(mutation) } } @@ -360,4 +360,4 @@ macro_rules! register_entities_without_relation { ($builder:expr, [$($module_paths:ident),+ $(,)?]) => { $(seaography::register_entity_without_relation!($builder, $module_paths);)* }; -} +} \ No newline at end of file diff --git a/src/query/cascading.rs b/src/query/cascading.rs new file mode 100644 index 00000000..e5dd2e6f --- /dev/null +++ b/src/query/cascading.rs @@ -0,0 +1,14 @@ +use async_graphql::dynamic::ValueAccessor; + +pub fn get_cascade_conditions(cascades: Option) -> Vec { + if let Some(cascades) = cascades { + cascades + .list() + .unwrap() + .iter() + .map(|field| field.string().unwrap().to_string()) + .collect::>() + } else { + Vec::new() + } +} \ No newline at end of file diff --git a/src/query/entity_object_relation.rs b/src/query/entity_object_relation.rs index a813c1da..ca500be2 100644 --- a/src/query/entity_object_relation.rs +++ b/src/query/entity_object_relation.rs @@ -6,11 +6,12 @@ use async_graphql::{ use heck::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, - PaginationInputBuilder, + apply_memory_pagination, get_filter_conditions, BuilderContext, EntityObjectBuilder, + FilterInputBuilder, GuardAction, HashableGroupKey, KeyComplex, OneToManyLoader, OneToOneLoader, + OrderInputBuilder, PaginationInputBuilder, }; /// This builder produces a GraphQL field for an SeaORM entity relationship @@ -32,11 +33,22 @@ 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 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( @@ -108,63 +120,57 @@ 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 pagination = ctx.args.get(&context.entity_query_field.pagination); - let pagination = - PaginationInputBuilder { context }.parse_object(pagination); + let loader = ctx.data_unchecked::>>(); - let connection: Connection = apply_memory_pagination(values, pagination); + 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, + }, + }; + + let values = loader.load_one(key).await?; + let pagination = ctx.args.get(&context.entity_query_field.pagination); + let pagination = PaginationInputBuilder { context }.parse_object(pagination); - Ok(Some(FieldValue::owned_any(connection))) - }) - }, - ), + let object = apply_memory_pagination::(values, pagination); + + Ok(Some(resolver_fn(object))) + }) + }), }; match relation_definition.is_owner { @@ -184,4 +190,4 @@ impl EntityObjectRelationBuilder { )), } } -} +} \ No newline at end of file diff --git a/src/query/entity_object_via_relation.rs b/src/query/entity_object_via_relation.rs index db84ad23..8bf1c280 100644 --- a/src/query/entity_object_via_relation.rs +++ b/src/query/entity_object_via_relation.rs @@ -8,11 +8,12 @@ use sea_orm::{ ColumnTrait, Condition, DatabaseConnection, EntityTrait, Iden, ModelTrait, QueryFilter, Related, }; +#[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, + OneToManyLoader, OneToOneLoader, OrderInputBuilder, PaginationInputBuilder, }; /// This builder produces a GraphQL field for an SeaORM entity related trait @@ -41,11 +42,21 @@ 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 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( @@ -122,83 +133,77 @@ 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); - - let order_by = ctx.args.get(&context.entity_query_field.order_by); - let order_by = OrderInputBuilder { context }.parse_object::(order_by); + // 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 pagination = ctx.args.get(&context.entity_query_field.pagination); - let pagination = - PaginationInputBuilder { context }.parse_object(pagination); + let stmt = if >::via().is_some() { + >::find_related() + } else { + R::find() + }; - let db = ctx.data::()?; + let filters = ctx.args.get(&context.entity_query_field.filters); + let filters = get_filter_conditions::(context, filters); - let connection = if is_via_relation { - // TODO optimize query - let condition = Condition::all().add(from_col.eq(parent.get(from_col))); + let order_by = ctx.args.get(&context.entity_query_field.order_by); + let order_by = OrderInputBuilder { context }.parse_object::(order_by); - 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 key = KeyComplex:: { - key: vec![parent.get(from_col)], - meta: HashableGroupKey:: { - stmt, - columns: vec![to_col], - filters: Some(filters), - order_by, - }, - }; + let db = ctx.data::()?; - let values = loader.load_one(key).await?; + let object = if is_via_relation { + // TODO optimize query + let condition = Condition::all().add(from_col.eq(parent.get(from_col))); - apply_memory_pagination(values, pagination) + 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 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 { @@ -218,4 +223,4 @@ impl EntityObjectViaRelationBuilder { )), } } -} +} \ No newline at end of file diff --git a/src/query/entity_query_field.rs b/src/query/entity_query_field.rs index 168b9da7..d96cf8e9 100644 --- a/src/query/entity_query_field.rs +++ b/src/query/entity_query_field.rs @@ -3,14 +3,19 @@ use async_graphql::{ Error, }; use heck::ToLowerCamelCase; -use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter}; +use sea_orm::{ + DatabaseConnection, EntityTrait, Iterable, JoinType, QueryFilter, QuerySelect, RelationTrait, +}; +#[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, BuilderContext, EntityObjectBuilder, + FilterInputBuilder, GuardAction, OrderInputBuilder, PaginationInputBuilder, }; +use super::get_cascade_conditions; + /// The configuration structure for EntityQueryFieldBuilder pub struct EntityQueryFieldConfig { /// used to format entity field name @@ -29,7 +34,7 @@ impl std::default::Default for EntityQueryFieldConfig { type_name: Box::new(|object_name: &str| -> String { object_name.to_lower_camel_case() }), - filters: "filters".into(), + filters: "filter".into(), order_by: "orderBy".into(), pagination: "pagination".into(), } @@ -61,6 +66,7 @@ impl EntityQueryFieldBuilder { T: EntityTrait, ::Model: Sync, { + #[cfg(not(feature = "offset-pagination"))] let connection_object_builder = ConnectionObjectBuilder { context: self.context, }; @@ -78,53 +84,96 @@ impl EntityQueryFieldBuilder { }; 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 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); + let cascades = ctx.args.get("cascade"); + let cascades = get_cascade_conditions(cascades); + let stmt = T::Relation::iter().fold(T::find(), |stmt, related_table| { + let related_table_name = related_table.def().to_tbl; + match related_table_name { + sea_orm::sea_query::TableRef::Table(iden) => { + if cascades.contains(&iden.to_string()) { + stmt.join(JoinType::InnerJoin, related_table.def()) + .distinct() + } else { + stmt + } + } + sea_orm::sea_query::TableRef::SchemaTable(_, iden) => { + if cascades.contains(&iden.to_string()) { + stmt.join(JoinType::InnerJoin, related_table.def()) + .distinct() + } else { + stmt + } + } + sea_orm::sea_query::TableRef::DatabaseSchemaTable(_, _, iden) => { + if cascades.contains(&iden.to_string()) { + stmt.join(JoinType::InnerJoin, related_table.def()) + .distinct() + } else { + stmt + } + } + _ => stmt, + } + }); + let stmt = stmt.filter(filters); + let stmt = apply_order(stmt, order_by); - let db = ctx.data::()?; + let db = ctx.data::()?; - let connection = apply_pagination::(db, stmt, pagination).await?; + let object = apply_pagination::(db, stmt, pagination).await?; - Ok(Some(FieldValue::owned_any(connection))) + Ok(Some(resolver_fn(object))) + } }) - }, - ) + } + }) + .argument(InputValue::new( + "cascade", + TypeRef::named_list(TypeRef::STRING), + )) .argument(InputValue::new( &self.context.entity_query_field.filters, TypeRef::named(filter_input_builder.type_name(&object_name)), @@ -138,4 +187,4 @@ impl EntityQueryFieldBuilder { TypeRef::named(pagination_input_builder.type_name()), )) } -} +} \ No newline at end of file diff --git a/src/query/mod.rs b/src/query/mod.rs index 4c9f9f15..b64258c8 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -18,3 +18,6 @@ pub use entity_object_relation::*; pub mod entity_object_via_relation; pub use entity_object_via_relation::*; + +pub mod cascading; +pub use cascading::*; \ 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