Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add page info field to connection object #1177

Merged
merged 7 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/torii/graphql/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub const EVENT_TYPE_NAME: &str = "World__Event";
pub const SOCIAL_TYPE_NAME: &str = "World__Social";
pub const CONTENT_TYPE_NAME: &str = "World__Content";
pub const METADATA_TYPE_NAME: &str = "World__Metadata";
pub const PAGE_INFO_TYPE_NAME: &str = "World__PageInfo";
pub const TRANSACTION_TYPE_NAME: &str = "World__Transaction";
pub const QUERY_TYPE_NAME: &str = "World__Query";
pub const SUBSCRIPTION_TYPE_NAME: &str = "World__Subscription";
Expand Down
10 changes: 10 additions & 0 deletions crates/torii/graphql/src/object/connection/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use async_graphql::connection::PageInfo;
use async_graphql::dynamic::indexmap::IndexMap;
use async_graphql::dynamic::{Field, InputValue, ResolverContext, TypeRef};
use async_graphql::{Error, Name, Value};
use sqlx::sqlite::SqliteRow;
use sqlx::Row;

use self::page_info::PageInfoObject;
use super::ObjectTrait;
use crate::constants::PAGE_INFO_TYPE_NAME;
use crate::query::order::Order;
use crate::query::value_mapping_from_row;
use crate::types::{GraphqlType, TypeData, TypeMapping, ValueMapping};
Expand Down Expand Up @@ -37,6 +41,10 @@ impl ConnectionObject {
TypeData::Simple(TypeRef::named_list(format!("{}Edge", type_name))),
),
(Name::new("total_count"), TypeData::Simple(TypeRef::named_nn(TypeRef::INT))),
(
Name::new("page_info"),
TypeData::Nested((TypeRef::named_nn(PAGE_INFO_TYPE_NAME), IndexMap::new())),
),
]);

Self {
Expand Down Expand Up @@ -109,6 +117,7 @@ pub fn connection_output(
id_column: &str,
total_count: i64,
is_external: bool,
page_info: PageInfo,
) -> sqlx::Result<ValueMapping> {
let model_edges = data
.iter()
Expand All @@ -134,5 +143,6 @@ pub fn connection_output(
Ok(ValueMapping::from([
(Name::new("total_count"), Value::from(total_count)),
(Name::new("edges"), Value::List(model_edges?)),
(Name::new("page_info"), PageInfoObject::value(page_info)),
]))
}
26 changes: 26 additions & 0 deletions crates/torii/graphql/src/object/connection/page_info.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use async_graphql::connection::PageInfo;
use async_graphql::dynamic::indexmap::IndexMap;
use async_graphql::dynamic::Field;
use async_graphql::{Name, Value};

use crate::mapping::PAGE_INFO_TYPE_MAPPING;
use crate::object::{ObjectTrait, TypeMapping};
Expand Down Expand Up @@ -26,3 +29,26 @@ impl ObjectTrait for PageInfoObject {
None
}
}

impl PageInfoObject {
pub fn value(page_info: PageInfo) -> Value {
Value::Object(IndexMap::from([
(Name::new("has_previous_page"), Value::from(page_info.has_previous_page)),
(Name::new("has_next_page"), Value::from(page_info.has_next_page)),
(
Name::new("start_cursor"),
match page_info.start_cursor {
Some(val) => Value::from(val),
None => Value::Null,
},
),
(
Name::new("end_cursor"),
match page_info.end_cursor {
Some(val) => Value::from(val),
None => Value::Null,
},
),
]))
}
}
4 changes: 3 additions & 1 deletion crates/torii/graphql/src/object/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ impl ObjectTrait for EntityObject {
let connection = parse_connection_arguments(&ctx)?;
let keys = parse_keys_argument(&ctx)?;
let total_count = count_rows(&mut conn, ENTITY_TABLE, &keys, &None).await?;
let data = fetch_multiple_rows(
let (data, page_info) = fetch_multiple_rows(
&mut conn,
ENTITY_TABLE,
EVENT_ID_COLUMN,
&keys,
&None,
&None,
&connection,
total_count,
)
.await?;
let results = connection_output(
Expand All @@ -69,6 +70,7 @@ impl ObjectTrait for EntityObject {
EVENT_ID_COLUMN,
total_count,
false,
page_info,
)?;

Ok(Some(Value::Object(results)))
Expand Down
4 changes: 3 additions & 1 deletion crates/torii/graphql/src/object/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ impl ObjectTrait for EventObject {
let connection = parse_connection_arguments(&ctx)?;
let keys = parse_keys_argument(&ctx)?;
let total_count = count_rows(&mut conn, EVENT_TABLE, &keys, &None).await?;
let data = fetch_multiple_rows(
let (data, page_info) = fetch_multiple_rows(
&mut conn,
EVENT_TABLE,
ID_COLUMN,
&keys,
&None,
&None,
&connection,
total_count,
)
.await?;
let results = connection_output(
Expand All @@ -66,6 +67,7 @@ impl ObjectTrait for EventObject {
ID_COLUMN,
total_count,
false,
page_info,
)?;

Ok(Some(Value::Object(results)))
Expand Down
17 changes: 12 additions & 5 deletions crates/torii/graphql/src/object/metadata/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use async_graphql::connection::PageInfo;
use async_graphql::dynamic::{Field, FieldFuture, TypeRef};
use async_graphql::{Name, Value};
use sqlx::sqlite::SqliteRow;
use sqlx::{Pool, Row, Sqlite};

use super::connection::page_info::PageInfoObject;
use super::connection::{connection_arguments, cursor, parse_connection_arguments};
use super::ObjectTrait;
use crate::constants::{
Expand Down Expand Up @@ -54,19 +56,21 @@ impl ObjectTrait for MetadataObject {
let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
let connection = parse_connection_arguments(&ctx)?;
let total_count = count_rows(&mut conn, &table_name, &None, &None).await?;
let data = fetch_multiple_rows(
let (data, page_info) = fetch_multiple_rows(
&mut conn,
&table_name,
ID_COLUMN,
&None,
&None,
&None,
&connection,
total_count,
)
.await?;

// convert json field to value_mapping expected by content object
let results = metadata_connection_output(&data, &type_mapping, total_count)?;
let results =
metadata_connection_output(&data, &type_mapping, total_count, page_info)?;

Ok(Some(Value::Object(results)))
})
Expand All @@ -85,6 +89,7 @@ fn metadata_connection_output(
data: &[SqliteRow],
types: &TypeMapping,
total_count: i64,
page_info: PageInfo,
) -> sqlx::Result<ValueMapping> {
let edges = data
.iter()
Expand All @@ -107,9 +112,10 @@ fn metadata_connection_output(

value_mapping.insert(Name::new("content"), Value::Object(content));

let mut edge = ValueMapping::new();
edge.insert(Name::new("node"), Value::Object(value_mapping));
edge.insert(Name::new("cursor"), Value::String(cursor));
let edge = ValueMapping::from([
(Name::new("node"), Value::Object(value_mapping)),
(Name::new("cursor"), Value::String(cursor)),
]);

Ok(Value::Object(edge))
})
Expand All @@ -118,6 +124,7 @@ fn metadata_connection_output(
Ok(ValueMapping::from([
(Name::new("total_count"), Value::from(total_count)),
(Name::new("edges"), Value::List(edges?)),
(Name::new("page_info"), PageInfoObject::value(page_info)),
]))
}

Expand Down
4 changes: 3 additions & 1 deletion crates/torii/graphql/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,15 @@ pub trait ObjectTrait: Send + Sync {
let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
let connection = parse_connection_arguments(&ctx)?;
let total_count = count_rows(&mut conn, &table_name, &None, &None).await?;
let data = fetch_multiple_rows(
let (data, page_info) = fetch_multiple_rows(
&mut conn,
&table_name,
ID_COLUMN,
&None,
&None,
&None,
&connection,
total_count,
)
.await?;
let results = connection_output(
Expand All @@ -113,6 +114,7 @@ pub trait ObjectTrait: Send + Sync {
ID_COLUMN,
total_count,
false,
page_info,
)?;

Ok(Some(Value::Object(results)))
Expand Down
17 changes: 12 additions & 5 deletions crates/torii/graphql/src/object/model_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,27 @@ impl ObjectTrait for ModelDataObject {
let connection = parse_connection_arguments(&ctx)?;
let id_column = "event_id";

let data = fetch_multiple_rows(
let total_count = count_rows(&mut conn, &type_name, &None, &filters).await?;
let (data, page_info) = fetch_multiple_rows(
&mut conn,
&type_name,
id_column,
&None,
&order,
&filters,
&connection,
total_count,
)
.await?;

let total_count = count_rows(&mut conn, &type_name, &None, &filters).await?;
let connection =
connection_output(&data, &type_mapping, &order, id_column, total_count, true)?;
let connection = connection_output(
&data,
&type_mapping,
&order,
id_column,
total_count,
true,
page_info,
)?;

Ok(Some(Value::Object(connection)))
})
Expand Down
89 changes: 85 additions & 4 deletions crates/torii/graphql/src/query/data.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use async_graphql::connection::PageInfo;
use sqlx::pool::PoolConnection;
use sqlx::sqlite::SqliteRow;
use sqlx::{Result, Sqlite};
use sqlx::{Result, Row, Sqlite};

use super::filter::{Filter, FilterValue};
use super::order::{CursorDirection, Direction, Order};
Expand Down Expand Up @@ -34,6 +35,7 @@ pub async fn fetch_single_row(
sqlx::query(&query).fetch_one(conn).await
}

#[allow(clippy::too_many_arguments)]
pub async fn fetch_multiple_rows(
conn: &mut PoolConnection<Sqlite>,
table_name: &str,
Expand All @@ -42,14 +44,17 @@ pub async fn fetch_multiple_rows(
order: &Option<Order>,
filters: &Option<Vec<Filter>>,
connection: &ConnectionArguments,
) -> Result<Vec<SqliteRow>> {
total_count: i64,
) -> Result<(Vec<SqliteRow>, PageInfo)> {
let mut conditions = build_conditions(keys, filters);

let mut cursor_param = &connection.after;
if let Some(after_cursor) = &connection.after {
conditions.push(handle_cursor(after_cursor, order, CursorDirection::After, id_column)?);
}

if let Some(before_cursor) = &connection.before {
cursor_param = &connection.before;
conditions.push(handle_cursor(before_cursor, order, CursorDirection::Before, id_column)?);
}

Expand All @@ -58,7 +63,18 @@ pub async fn fetch_multiple_rows(
query.push_str(&format!(" WHERE {}", conditions.join(" AND ")));
}

let limit = connection.first.or(connection.last).or(connection.limit).unwrap_or(DEFAULT_LIMIT);
let is_cursor_based = connection.first.or(connection.last).is_some() || cursor_param.is_some();

let data_limit =
connection.first.or(connection.last).or(connection.limit).unwrap_or(DEFAULT_LIMIT);
let limit = if is_cursor_based {
match &cursor_param {
Some(_) => data_limit + 2,
None => data_limit + 1, // prev page does not exist
}
} else {
data_limit
};

// NOTE: Order is determined by the `order` param if provided, otherwise it's inferred from the
// `first` or `last` param. Explicit ordering take precedence
Expand Down Expand Up @@ -89,7 +105,72 @@ pub async fn fetch_multiple_rows(
query.push_str(&format!(" OFFSET {}", offset));
}

sqlx::query(&query).fetch_all(conn).await
let mut data = sqlx::query(&query).fetch_all(conn).await?;
let mut page_info = PageInfo {
has_previous_page: false,
has_next_page: false,
start_cursor: None,
end_cursor: None,
};

if data.is_empty() {
Ok((data, page_info))
} else if is_cursor_based {
let order_field = match order {
Some(order) => format!("external_{}", order.field),
None => id_column.to_string(),
};

match cursor_param {
Some(cursor_query) => {
let first_cursor = cursor::encode(
&data[0].try_get::<String, &str>(id_column)?,
&data[0].try_get_unchecked::<String, &str>(&order_field)?,
);

if &first_cursor == cursor_query && data.len() != 1 {
data.remove(0);
page_info.has_previous_page = true;
} else {
data.pop();
}

if data.len() as u64 == limit - 1 {
page_info.has_next_page = true;
data.pop();
}
}
None => {
if data.len() as u64 == limit {
page_info.has_next_page = true;
data.pop();
}
}
}

if !data.is_empty() {
page_info.start_cursor = Some(cursor::encode(
&data[0].try_get::<String, &str>(id_column)?,
&data[0].try_get_unchecked::<String, &str>(&order_field)?,
));
page_info.end_cursor = Some(cursor::encode(
&data[data.len() - 1].try_get::<String, &str>(id_column)?,
&data[data.len() - 1].try_get_unchecked::<String, &str>(&order_field)?,
));
}

Ok((data, page_info))
} else {
let offset = connection.offset.unwrap_or(0);
if 1 < offset && offset < total_count as u64 {
page_info.has_previous_page = true;
}
if limit + offset < total_count as u64 {
page_info.has_next_page = true;
}

Ok((data, page_info))
}
}

fn handle_cursor(
Expand Down
4 changes: 2 additions & 2 deletions crates/torii/graphql/src/query/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ pub struct Order {

#[derive(AsRefStr, Debug, EnumString)]
pub enum CursorDirection {
#[strum(serialize = "<")]
#[strum(serialize = "<=")]
After,
#[strum(serialize = ">")]
#[strum(serialize = ">=")]
Before,
}
Loading
Loading