diff --git a/README.md b/README.md index 17ec6174..46721216 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ Go to http://localhost:8000/ and try out the following queries: ```graphql { - film(pagination: { limit: 10, page: 0 }, orderBy: { title: ASC }) { - data { + film(pagination: { pages: { limit: 10, page: 0 } }, orderBy: { title: ASC }) { + nodes { title description releaseYear @@ -79,7 +79,7 @@ Go to http://localhost:8000/ and try out the following queries: ```graphql { store(filters: { storeId: { eq: 1 } }) { - data { + nodes { storeId address { address @@ -98,8 +98,11 @@ Go to http://localhost:8000/ and try out the following queries: ```graphql { - customer(filters: { active: { eq: 0 } }, pagination: { page: 2, limit: 3 }) { - data { + customer( + filters: { active: { eq: 0 } } + pagination: { pages: { page: 2, limit: 3 } } + ) { + nodes { customerId lastName email @@ -110,6 +113,28 @@ Go to http://localhost:8000/ and try out the following queries: } ``` +### The query above using cursor pagination + +```graphql +{ + customer( + filters: { active: { eq: 0 } } + pagination: { cursor: { limit: 3, cursor: "Int[3]:271" } } + ) { + nodes { + customerId + lastName + email + } + pageInfo { + hasPreviousPage + hasNextPage + endCursor + } + } +} +``` + ### Postgres Setup the [sakila](https://github.com/SeaQL/seaography/blob/main/examples/postgres/sakila-schema.sql) sample database. diff --git a/derive/src/root_query.rs b/derive/src/root_query.rs index 6971f446..21fd555e 100644 --- a/derive/src/root_query.rs +++ b/derive/src/root_query.rs @@ -1,5 +1,4 @@ -use heck::ToUpperCamelCase; -use proc_macro2::TokenStream; +use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; #[derive(Debug, Eq, PartialEq, bae::FromAttributes)] @@ -24,82 +23,501 @@ pub fn root_query_fn( }) .collect::, crate::error::Error>>()?; - let names: Vec = paths + let queries: Vec = paths .iter() .map(|path| { - let name = path.clone().into_iter().last().unwrap().to_string(); - let name = format!("Paginated{}Result", name.to_upper_camel_case()); + let name = format_ident!("{}", path.clone().into_iter().last().unwrap().to_string()); + + let basic_query = basic_query(&name, path); quote! { - #[graphql(concrete(name = #name, params(#path::Model)))] + #basic_query } }) .collect(); - let queries: Vec = paths - .iter() - .map(|path| { - let name = format_ident!("{}", path.clone().into_iter().last().unwrap().to_string()); + let basic_dependencies = basic_dependencies(); + Ok(quote! { + #basic_dependencies - quote!{ - pub async fn #name<'a>( - &self, - ctx: &async_graphql::Context<'a>, - filters: Option<#path::Filter>, - pagination: Option, - order_by: Option<#path::OrderBy>, - ) -> PaginatedResult<#path::Model> { - use sea_orm::prelude::*; + #[async_graphql::Object] + impl #ident { + #(#queries)* + } + }) +} + +pub fn basic_query(name: &Ident, path: &TokenStream) -> TokenStream { + quote! { + pub async fn #name<'a>( + &self, + ctx: &async_graphql::Context<'a>, + filters: Option<#path::Filter>, + pagination: Option, + order_by: Option<#path::OrderBy>, + ) -> async_graphql::types::connection::Connection { + use sea_orm::prelude::*; + use sea_orm::Iterable; + use seaography::itertools::Itertools; + use async_graphql::types::connection::CursorType; + + println!("filters: {:?}", filters); + + let db: &crate::DatabaseConnection = ctx.data::().unwrap(); + let stmt = #path::Entity::find() + .filter(#path::filter_recursive(filters)); + + let stmt = #path::order_by(stmt, order_by); - let db: &crate::DatabaseConnection = ctx.data::().unwrap(); - let stmt = #path::Entity::find() - .filter(#path::filter_recursive(filters)); + fn get_result( + data: Vec<#path::Model>, + has_previous_page: bool, + has_next_page: bool, + pages: Option, + current: Option + ) -> async_graphql::types::connection::Connection< + String, + #path::Model, + ExtraPaginationFields, + async_graphql::types::connection::EmptyFields + > { + let edges: Vec> = data + .into_iter() + .map(|node| { + let values: Vec = #path::PrimaryKey::iter() + .map(|variant| { + node.get(variant.into_column()) + }) + .collect(); - let stmt = #path::order_by(stmt, order_by); + let cursor_string = CursorValues(values).encode_cursor(); - if let Some(pagination) = pagination { + async_graphql::types::connection::Edge::new(cursor_string, node) + }) + .collect(); + + let mut result = async_graphql::types::connection::Connection::< + String, + #path::Model, + ExtraPaginationFields, + async_graphql::types::connection::EmptyFields + >::with_additional_fields( + has_previous_page, + has_next_page, + ExtraPaginationFields { + pages, + current + } + ); + + result.edges.extend(edges); + + result + } + + if let Some(pagination) = pagination { + + match pagination { + Pagination::Pages(pagination) => { let paginator = stmt.paginate(db, pagination.limit); - let data: Vec<#path::Model> = - paginator.fetch_page(pagination.page).await.unwrap(); - let pages = paginator.num_pages().await.unwrap(); - PaginatedResult { - data, - pages, - current: pagination.page, + + let data: Vec<#path::Model> = paginator + .fetch_page(pagination.page) + .await + .unwrap(); + + let pages = paginator + .num_pages() + .await + .unwrap(); + + get_result(data, pagination.page != 1, pagination.page < pages, Some(pages), Some(pagination.page)) + }, + Pagination::Cursor(cursor) => { + let next_stmt = stmt.clone(); + let previous_stmt = stmt.clone(); + + fn apply_stmt_cursor_by(stmt: sea_orm::entity::prelude::Select<#path::Entity>) -> sea_orm::Cursor> { + if #path::PrimaryKey::iter().len() == 1 { + let column = #path::PrimaryKey::iter().map(|variant| variant.into_column()).collect::>()[0]; + stmt.cursor_by(column) + } else if #path::PrimaryKey::iter().len() == 2 { + let columns = #path::PrimaryKey::iter().map(|variant| variant.into_column()).collect_tuple::<(#path::Column, #path::Column)>().unwrap(); + stmt.cursor_by(columns) + } else if #path::PrimaryKey::iter().len() == 3 { + let columns = #path::PrimaryKey::iter().map(|variant| variant.into_column()).collect_tuple::<(#path::Column, #path::Column, #path::Column)>().unwrap(); + stmt.cursor_by(columns) + } else { + panic!("seaography does not support cursors with size greater than 3") + } } - } else { - let data: Vec<#path::Model> = stmt.all(db).await.unwrap(); - PaginatedResult { - data, - pages: 1, - current: 1, + + let mut stmt = apply_stmt_cursor_by(stmt); + + if let Some(cursor_string) = cursor.cursor { + let values = CursorValues::decode_cursor(cursor_string.as_str()).unwrap(); + + let cursor_values: sea_orm::sea_query::value::ValueTuple = map_cursor_values(values.0); + + stmt.after(cursor_values); } - } + let data = stmt + .first(cursor.limit) + .all(db) + .await + .unwrap(); + + let has_next_page: bool = { + let mut next_stmt = apply_stmt_cursor_by(next_stmt); + + let last_node = data.last(); + + if let Some(node) = last_node { + let values: Vec = #path::PrimaryKey::iter() + .map(|variant| { + node.get(variant.into_column()) + }) + .collect(); + + let values = map_cursor_values(values); + + let next_data = next_stmt + .first(cursor.limit) + .after(values) + .all(db) + .await + .unwrap(); + + next_data.len() != 0 + } else { + false + } + }; + + let has_previous_page: bool = { + let mut previous_stmt = apply_stmt_cursor_by(previous_stmt); + + let first_node = data.first(); + + if let Some(node) = first_node { + let values: Vec = #path::PrimaryKey::iter() + .map(|variant| { + node.get(variant.into_column()) + }) + .collect(); + + let values = map_cursor_values(values); + + let previous_data = previous_stmt + .first(cursor.limit) + .before(values) + .all(db) + .await + .unwrap(); + + previous_data.len() != 0 + } else { + false + } + }; + + get_result(data, has_previous_page, has_next_page, None, None) + } } + } else { + let data: Vec<#path::Model> = stmt.all(db).await.unwrap(); + + get_result(data, false, false, Some(1), Some(1)) } - }) - .collect(); + } + } +} - Ok(quote! { +pub fn basic_dependencies() -> TokenStream { + quote! { #[derive(Debug, async_graphql::InputObject)] - pub struct PaginationInput { + pub struct PageInput { pub limit: usize, pub page: usize, } - #[derive(Debug, async_graphql::SimpleObject)] - #(#names)* - pub struct PaginatedResult { - pub data: Vec, - pub pages: usize, - pub current: usize, + #[derive(Debug, async_graphql::InputObject)] + pub struct CursorInput { + pub cursor: Option, + pub limit: u64, } - #[async_graphql::Object] - impl #ident { - #(#queries)* + #[derive(async_graphql::OneofObject)] + pub enum Pagination { + Pages(PageInput), + Cursor(CursorInput), } - }) + + + #[derive(async_graphql::SimpleObject)] + pub struct ExtraPaginationFields { + pub pages: Option, + pub current: Option, + } + + #[derive(Debug)] + pub enum DecodeMode { + Type, + Length, + ColonSkip, + Data, + } + + pub fn map_cursor_values(values: Vec) -> sea_orm::sea_query::value::ValueTuple { + use seaography::itertools::Itertools; + + if values.len() == 1 { + sea_orm::sea_query::value::ValueTuple::One(values[0].clone()) + } else if values.len() == 2 { + sea_orm::sea_query::value::ValueTuple::Two(values[0].clone(), values[1].clone()) + } else if values.len() == 3 { + sea_orm::sea_query::value::ValueTuple::Three(values[0].clone(), values[1].clone(), values[2].clone()) + } else { + panic!("seaography does not support cursors values with size greater than 3") + } + } + + #[derive(Debug)] + pub struct CursorValues(pub Vec); + + impl async_graphql::types::connection::CursorType for CursorValues { + type Error = String; + + fn decode_cursor(s: &str) -> Result { + let chars = s.chars(); + + let mut values: Vec = vec![]; + + let mut type_indicator = String::new(); + let mut length_indicator = String::new(); + let mut data_buffer = String::new(); + let mut length = -1; + + let mut mode: DecodeMode = DecodeMode::Type; + for char in chars { + match mode { + DecodeMode::Type => { + if char.eq(&'[') { + mode = DecodeMode::Length; + } else if char.eq(&',') { + // SKIP + } else { + type_indicator.push(char); + } + }, + DecodeMode::Length => { + if char.eq(&']') { + mode = DecodeMode::ColonSkip; + length = length_indicator.parse::().unwrap(); + } else { + length_indicator.push(char); + } + }, + DecodeMode::ColonSkip => { + // skips ':' char + mode = DecodeMode::Data; + }, + DecodeMode::Data => { + if length > 0 { + data_buffer.push(char); + length -= 1; + } + + if length <= 0{ + let value: sea_orm::Value = match type_indicator.as_str() { + "TinyInt" => { + if length.eq(&-1) { + sea_orm::Value::TinyInt(None) + } else { + sea_orm::Value::TinyInt(Some(data_buffer.parse::().unwrap())) + } + }, + "SmallInt" => { + if length.eq(&-1) { + sea_orm::Value::SmallInt(None) + } else { + sea_orm::Value::SmallInt(Some(data_buffer.parse::().unwrap())) + } + }, + "Int" => { + if length.eq(&-1) { + sea_orm::Value::Int(None) + } else { + sea_orm::Value::Int(Some(data_buffer.parse::().unwrap())) + } + }, + "BigInt" => { + if length.eq(&-1) { + sea_orm::Value::BigInt(None) + } else { + sea_orm::Value::BigInt(Some(data_buffer.parse::().unwrap())) + } + }, + "TinyUnsigned" => { + if length.eq(&-1) { + sea_orm::Value::TinyUnsigned(None) + } else { + sea_orm::Value::TinyUnsigned(Some(data_buffer.parse::().unwrap())) + } + }, + "SmallUnsigned" => { + if length.eq(&-1) { + sea_orm::Value::SmallUnsigned(None) + } else { + sea_orm::Value::SmallUnsigned(Some(data_buffer.parse::().unwrap())) + } + }, + "Unsigned" => { + if length.eq(&-1) { + sea_orm::Value::Unsigned(None) + } else { + sea_orm::Value::Unsigned(Some(data_buffer.parse::().unwrap())) + } + }, + "BigUnsigned" => { + if length.eq(&-1) { + sea_orm::Value::BigUnsigned(None) + } else { + sea_orm::Value::BigUnsigned(Some(data_buffer.parse::().unwrap())) + } + }, + "String" => { + if length.eq(&-1) { + sea_orm::Value::String(None) + } else { + sea_orm::Value::String(Some(Box::new(data_buffer.parse::().unwrap()))) + } + }, + "Uuid" => { + if length.eq(&-1) { + sea_orm::Value::Uuid(None) + } else { + sea_orm::Value::Uuid(Some(Box::new(data_buffer.parse::().unwrap()))) + } + }, + _ => { + // FIXME: missing value types + panic!("cannot encode current type") + }, + }; + + values.push(value); + + type_indicator = String::new(); + length_indicator = String::new(); + data_buffer = String::new(); + length = -1; + + mode = DecodeMode::Type; + } + } + } + } + + Ok(Self(values)) + } + + fn encode_cursor(&self) -> String { + use seaography::itertools::Itertools; + + self.0.iter().map(|value| -> String { + match value { + sea_orm::Value::TinyInt(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("TinyInt[{}]:{}", value.len(), value) + } else { + format!("TinyInt[-1]:") + } + }, + sea_orm::Value::SmallInt(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("SmallInt[{}]:{}", value.len(), value) + } else { + format!("SmallInt[-1]:") + } + }, + sea_orm::Value::Int(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("Int[{}]:{}", value.len(), value) + } else { + format!("Int[-1]:") + } + }, + sea_orm::Value::BigInt(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("BigInt[{}]:{}", value.len(), value) + } else { + format!("BigInt[-1]:") + } + }, + sea_orm::Value::TinyUnsigned(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("TinyUnsigned[{}]:{}", value.len(), value) + } else { + format!("TinyUnsigned[-1]:") + } + }, + sea_orm::Value::SmallUnsigned(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("SmallUnsigned[{}]:{}", value.len(), value) + } else { + format!("SmallUnsigned[-1]:") + } + }, + sea_orm::Value::Unsigned(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("Unsigned[{}]:{}", value.len(), value) + } else { + format!("Unsigned[-1]:") + } + }, + sea_orm::Value::BigUnsigned(value) => { + if let Some(value) = value { + let value = value.to_string(); + format!("BigUnsigned[{}]:{}", value.len(), value) + } else { + format!("BigUnsigned[-1]:") + } + }, + sea_orm::Value::String(value) => { + if let Some(value) = value { + let value = value.as_ref(); + format!("String[{}]:{}", value.len(), value) + } else { + format!("String[-1]:") + } + }, + sea_orm::Value::Uuid(value) => { + if let Some(value) = value { + let value = value.as_ref().to_string(); + format!("Uuid[{}]:{}", value.len(), value) + } else { + format!("Uuid[-1]:") + } + }, + _ => { + // FIXME: missing value types + panic!("cannot + current type") + }, + } + }) + .join(",") + } + } + } } diff --git a/examples/mysql/tests/query_tests.rs b/examples/mysql/tests/query_tests.rs index b63cbf3b..7f1ae385 100644 --- a/examples/mysql/tests/query_tests.rs +++ b/examples/mysql/tests/query_tests.rs @@ -35,24 +35,24 @@ async fn test_simple_query() { schema .execute( r#" - { - store { - data { - storeId - staff { - firstName - lastName - } - } - } + { + store { + nodes { + storeId + staff { + firstName + lastName } - "#, + } + } + } + "#, ) .await, r#" { "store": { - "data": [ + "nodes": [ { "storeId": 1, "staff": { @@ -70,7 +70,7 @@ async fn test_simple_query() { ] } } - "#, + "#, ) } @@ -82,24 +82,24 @@ async fn test_simple_query_with_filter() { schema .execute( r#" - { - store(filters: {storeId:{eq: 1}}) { - data { - storeId - staff { - firstName - lastName - } + { + store(filters: {storeId:{eq: 1}}) { + nodes { + storeId + staff { + firstName + lastName } } - } - "#, + } + } + "#, ) .await, r#" { "store": { - "data": [ + "nodes": [ { "storeId": 1, "staff": { @@ -110,7 +110,7 @@ async fn test_simple_query_with_filter() { ] } } - "#, + "#, ) } @@ -122,37 +122,40 @@ async fn test_filter_with_pagination() { schema .execute( r#" - { - customer (filters:{active:{eq: 0}}, pagination:{page: 2, limit: 3}) { - data { - customerId + { + customer( + filters: { active: { eq: 0 } } + pagination: { pages: { page: 2, limit: 3 } } + ) { + nodes { + customerId + } + pages + current } - pages - current } - } - "#, + "#, ) .await, r#" - { - "customer": { - "data": [ - { - "customerId": 315 - }, - { - "customerId": 368 - }, - { - "customerId": 406 - } - ], - "pages": 5, - "current": 2 - } + { + "customer": { + "nodes": [ + { + "customerId": 315 + }, + { + "customerId": 368 + }, + { + "customerId": 406 + } + ], + "pages": 5, + "current": 2 } - "#, + } + "#, ) } @@ -165,8 +168,11 @@ async fn test_complex_filter_with_pagination() { .execute( r#" { - payment(filters:{amount: { gt: "11.1" }}, pagination: {limit: 2, page: 3}) { - data { + payment( + filters: { amount: { gt: "11.1" } } + pagination: { pages: { limit: 2, page: 3 } } + ) { + nodes { paymentId amount } @@ -174,26 +180,266 @@ async fn test_complex_filter_with_pagination() { current } } - "#, + "#, + ) + .await, + r#" + { + "payment": { + "nodes": [ + { + "paymentId": 8272, + "amount": "11.99" + }, + { + "paymentId": 9803, + "amount": "11.99" + } + ], + "pages": 5, + "current": 3 + } + } + "#, + ) +} + +#[tokio::test] +async fn test_cursor_pagination() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 5 } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, + ) + .await, + r#" + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 342, + "amount": "11.99", + "customer": { + "firstName": "KAREN" + } + } + }, + { + "node": { + "paymentId": 3146, + "amount": "11.99", + "customer": { + "firstName": "VICTORIA" + } + } + }, + { + "node": { + "paymentId": 5280, + "amount": "11.99", + "customer": { + "firstName": "VANESSA" + } + } + }, + { + "node": { + "paymentId": 5281, + "amount": "11.99", + "customer": { + "firstName": "ALMA" + } + } + }, + { + "node": { + "paymentId": 5550, + "amount": "11.99", + "customer": { + "firstName": "ROSEMARY" + } + } + } + ], + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "Int[3]:342", + "endCursor": "Int[4]:5550" + } + } + } + "#, + ) +} + +#[tokio::test] +async fn test_cursor_pagination_prev() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 3, cursor: "SmallUnsigned[4]:5550" } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, ) .await, r#" - { - "payment": { - "data": [ - { - "paymentId": 8272, - "amount": "11.99" - }, - { - "paymentId": 9803, - "amount": "11.99" - } - ], - "pages": 5, - "current": 3 + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 6409, + "amount": "11.99", + "customer": { + "firstName": "TANYA" + } + } + }, + { + "node": { + "paymentId": 8272, + "amount": "11.99", + "customer": { + "firstName": "RICHARD" + } + } + }, + { + "node": { + "paymentId": 9803, + "amount": "11.99", + "customer": { + "firstName": "NICHOLAS" + } + } } + ], + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "Int[4]:6409", + "endCursor": "Int[4]:9803" } - "#, + } + } + "#, ) } + +#[tokio::test] +async fn test_cursor_pagination_no_next() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 3, cursor: "SmallUnsigned[4]:9803" } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, + ) + .await, + r#" + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 15821, + "amount": "11.99", + "customer": { + "firstName": "KENT" + } + } + }, + { + "node": { + "paymentId": 15850, + "amount": "11.99", + "customer": { + "firstName": "TERRANCE" + } + } + } + ], + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "Int[5]:15821", + "endCursor": "Int[5]:15850" + } + } + } + "#, + ) +} \ No newline at end of file diff --git a/examples/postgres/.env b/examples/postgres/.env new file mode 100644 index 00000000..869d217e --- /dev/null +++ b/examples/postgres/.env @@ -0,0 +1,3 @@ +DATABASE_URL="postgres://postgres:postgres@127.0.0.1/sakila?currentSchema=public" +# COMPLEXITY_LIMIT= +# DEPTH_LIMIT= \ No newline at end of file diff --git a/examples/postgres/tests/query_tests.rs b/examples/postgres/tests/query_tests.rs index 66e97488..0629fae9 100644 --- a/examples/postgres/tests/query_tests.rs +++ b/examples/postgres/tests/query_tests.rs @@ -35,9 +35,56 @@ async fn test_simple_query() { schema .execute( r#" - { - store { - data { + { + store { + nodes { + storeId + staff { + firstName + lastName + } + } + } + } + "#, + ) + .await, + r#" + { + "store": { + "nodes": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" + } + }, + { + "storeId": 2, + "staff": { + "firstName": "Jon", + "lastName": "Stephens" + } + } + ] + } + } + "#, + ) +} + +#[tokio::test] +async fn test_simple_query_with_filter() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + store(filters: {storeId:{eq: 1}}) { + nodes { storeId staff { firstName @@ -45,119 +92,292 @@ async fn test_simple_query() { } } } - } - "#, + } + "#, ) .await, r#" - { - "store": { - "data": [ - { - "storeId": 1, - "staff": { - "firstName": "Mike", - "lastName": "Hillyer" - } - }, - { - "storeId": 2, - "staff": { - "firstName": "Jon", - "lastName": "Stephens" - } + { + "store": { + "nodes": [ + { + "storeId": 1, + "staff": { + "firstName": "Mike", + "lastName": "Hillyer" } - ] - } + } + ] } - "#, + } + "#, ) } #[tokio::test] -async fn test_simple_query_with_filter() { +async fn test_filter_with_pagination() { let schema = get_schema().await; assert_eq( schema .execute( r#" - { - store(filters: {storeId:{eq: 1}}) { - data { - storeId - staff { - firstName - lastName + { + customer( + filters: { active: { eq: 0 } } + pagination: { pages: { page: 2, limit: 3 } } + ) { + nodes { + customerId } + pages + current } } - } - "#, + "#, ) .await, r#" - { - "store": { - "data": [ - { - "storeId": 1, - "staff": { - "firstName": "Mike", - "lastName": "Hillyer" + { + "customer": { + "nodes": [ + { + "customerId": 315 + }, + { + "customerId": 368 + }, + { + "customerId": 406 + } + ], + "pages": 5, + "current": 2 + } + } + "#, + ) +} + +#[tokio::test] +async fn test_complex_filter_with_pagination() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11.1" } } + pagination: { pages: { limit: 2, page: 3 } } + ) { + nodes { + paymentId + amount } + pages + current } - ] + } + "#, + ) + .await, + r#" + { + "payment": { + "nodes": [ + { + "paymentId": 8272, + "amount": "11.9900" + }, + { + "paymentId": 9803, + "amount": "11.9900" } - } - "#, + ], + "pages": 5, + "current": 3 + } + } + "#, ) } #[tokio::test] -async fn test_filter_with_pagination() { +async fn test_cursor_pagination() { let schema = get_schema().await; assert_eq( schema .execute( r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 5 } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, + ) + .await, + r#" + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 342, + "amount": "11.9900", + "customer": { + "firstName": "KAREN" + } + } + }, { - customer (filters:{active:{eq: 0}}, pagination:{page: 2, limit: 3}) { - data { - customerId + "node": { + "paymentId": 3146, + "amount": "11.9900", + "customer": { + "firstName": "VICTORIA" + } + } + }, + { + "node": { + "paymentId": 5280, + "amount": "11.9900", + "customer": { + "firstName": "VANESSA" + } + } + }, + { + "node": { + "paymentId": 5281, + "amount": "11.9900", + "customer": { + "firstName": "ALMA" + } + } + }, + { + "node": { + "paymentId": 5550, + "amount": "11.9900", + "customer": { + "firstName": "ROSEMARY" } - pages - current } } - "#, + ], + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "Int[3]:342", + "endCursor": "Int[4]:5550" + } + } + } + "#, + ) +} + +#[tokio::test] +async fn test_cursor_pagination_prev() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 3, cursor: "SmallUnsigned[4]:5550" } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, ) .await, r#" - { - "customer": { - "data": [ - { - "customerId": 315 - }, - { - "customerId": 368 - }, - { - "customerId": 406 - } - ], - "pages": 5, - "current": 2 + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 6409, + "amount": "11.9900", + "customer": { + "firstName": "TANYA" + } + } + }, + { + "node": { + "paymentId": 8272, + "amount": "11.9900", + "customer": { + "firstName": "RICHARD" + } + } + }, + { + "node": { + "paymentId": 9803, + "amount": "11.9900", + "customer": { + "firstName": "NICHOLAS" + } + } } + ], + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "Int[4]:6409", + "endCursor": "Int[4]:9803" } - "#, + } + } + "#, ) } #[tokio::test] -async fn test_complex_filter_with_pagination() { +async fn test_cursor_pagination_no_next() { let schema = get_schema().await; assert_eq( @@ -165,35 +385,61 @@ async fn test_complex_filter_with_pagination() { .execute( r#" { - payment(filters:{amount: { gt: "11.1" }}, pagination: {limit: 2, page: 3}) { - data { - paymentId - amount + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 3, cursor: "SmallUnsigned[4]:9803" } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor } - pages - current } } - "#, + "#, ) .await, r#" - { - "payment": { - "data": [ - { - "paymentId": 8272, - "amount": "11.9900" - }, - { - "paymentId": 9803, - "amount": "11.9900" - } - ], - "pages": 5, - "current": 3 + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 15821, + "amount": "11.9900", + "customer": { + "firstName": "KENT" + } + } + }, + { + "node": { + "paymentId": 15850, + "amount": "11.9900", + "customer": { + "firstName": "TERRANCE" + } + } } + ], + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "Int[5]:15821", + "endCursor": "Int[5]:15850" } - "#, + } + } + "#, ) -} +} \ No newline at end of file diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index d63771dd..a42ce9db 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -4,8 +4,8 @@ name = 'seaography-sqlite-example' version = '0.1.0' [dependencies] -async-graphql = { version = "4.0.10", features = ["decimal", "chrono", "dataloader"] } -async-graphql-poem = { version = "4.0.10" } +async-graphql = { version = "4.0.14", features = ["decimal", "chrono", "dataloader"] } +async-graphql-poem = { version = "4.0.14" } async-trait = { version = "0.1.53" } dotenv = "0.15.0" poem = { version = "1.3.29" } diff --git a/examples/sqlite/tests/query_tests.rs b/examples/sqlite/tests/query_tests.rs index 7e239d04..f205bd23 100644 --- a/examples/sqlite/tests/query_tests.rs +++ b/examples/sqlite/tests/query_tests.rs @@ -35,7 +35,7 @@ async fn test_simple_query() { r#" { store { - data { + nodes { storeId staff { firstName @@ -50,7 +50,7 @@ async fn test_simple_query() { r#" { "store": { - "data": [ + "nodes": [ { "storeId": 1, "staff": { @@ -82,7 +82,7 @@ async fn test_simple_query_with_filter() { r#" { store(filters: {storeId:{eq: 1}}) { - data { + nodes { storeId staff { firstName @@ -97,7 +97,7 @@ async fn test_simple_query_with_filter() { r#" { "store": { - "data": [ + "nodes": [ { "storeId": 1, "staff": { @@ -120,22 +120,25 @@ async fn test_filter_with_pagination() { schema .execute( r#" - { - customer (filters:{active:{eq: 0}}, pagination:{page: 2, limit: 3}) { - data { - customerId + { + customer( + filters: { active: { eq: 0 } } + pagination: { pages: { page: 2, limit: 3 } } + ) { + nodes { + customerId + } + pages + current + } } - pages - current - } - } "#, ) .await, r#" { "customer": { - "data": [ + "nodes": [ { "customerId": 315 }, @@ -162,23 +165,26 @@ async fn test_complex_filter_with_pagination() { schema .execute( r#" - { - payment(filters:{amount: { gt: "11.1" }}, pagination: {limit: 2, page: 3}) { - data { - paymentId - amount + { + payment( + filters: { amount: { gt: "11.1" } } + pagination: { pages: { limit: 2, page: 3 } } + ) { + nodes { + paymentId + amount + } + pages + current } - pages - current } - } "#, ) .await, r#" { "payment": { - "data": [ + "nodes": [ { "paymentId": 8272, "amount": "11.99" @@ -195,3 +201,243 @@ async fn test_complex_filter_with_pagination() { "#, ) } + +#[tokio::test] +async fn test_cursor_pagination() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 5 } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, + ) + .await, + r#" + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 342, + "amount": "11.99", + "customer": { + "firstName": "KAREN" + } + } + }, + { + "node": { + "paymentId": 3146, + "amount": "11.99", + "customer": { + "firstName": "VICTORIA" + } + } + }, + { + "node": { + "paymentId": 5280, + "amount": "11.99", + "customer": { + "firstName": "VANESSA" + } + } + }, + { + "node": { + "paymentId": 5281, + "amount": "11.99", + "customer": { + "firstName": "ALMA" + } + } + }, + { + "node": { + "paymentId": 5550, + "amount": "11.99", + "customer": { + "firstName": "ROSEMARY" + } + } + } + ], + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": true, + "startCursor": "Int[3]:342", + "endCursor": "Int[4]:5550" + } + } + } + "#, + ) +} + +#[tokio::test] +async fn test_cursor_pagination_prev() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 3, cursor: "SmallUnsigned[4]:5550" } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, + ) + .await, + r#" + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 6409, + "amount": "11.99", + "customer": { + "firstName": "TANYA" + } + } + }, + { + "node": { + "paymentId": 8272, + "amount": "11.99", + "customer": { + "firstName": "RICHARD" + } + } + }, + { + "node": { + "paymentId": 9803, + "amount": "11.99", + "customer": { + "firstName": "NICHOLAS" + } + } + } + ], + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": true, + "startCursor": "Int[4]:6409", + "endCursor": "Int[4]:9803" + } + } + } + "#, + ) +} + +#[tokio::test] +async fn test_cursor_pagination_no_next() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + payment( + filters: { amount: { gt: "11" } } + pagination: { cursor: { limit: 3, cursor: "SmallUnsigned[4]:9803" } } + ) { + edges { + node { + paymentId + amount + customer { + firstName + } + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + "#, + ) + .await, + r#" + { + "payment": { + "edges": [ + { + "node": { + "paymentId": 15821, + "amount": "11.99", + "customer": { + "firstName": "KENT" + } + } + }, + { + "node": { + "paymentId": 15850, + "amount": "11.99", + "customer": { + "firstName": "TERRANCE" + } + } + } + ], + "pageInfo": { + "hasPreviousPage": true, + "hasNextPage": false, + "startCursor": "Int[5]:15821", + "endCursor": "Int[5]:15850" + } + } + } + "#, + ) +} diff --git a/generator/src/_Cargo.toml b/generator/src/_Cargo.toml index 94e0738d..268c74b2 100644 --- a/generator/src/_Cargo.toml +++ b/generator/src/_Cargo.toml @@ -4,8 +4,8 @@ name = '' version = '0.1.0' [dependencies] -async-graphql = { version = "4.0.10", features = ["decimal", "chrono", "dataloader"] } -async-graphql-poem = { version = "4.0.10" } +async-graphql = { version = "4.0.14", features = ["decimal", "chrono", "dataloader"] } +async-graphql-poem = { version = "4.0.14" } async-trait = { version = "0.1.53" } dotenv = "0.15.0" poem = { version = "1.3.29" }