Skip to content

Commit

Permalink
Add page info field to connection object (#1177)
Browse files Browse the repository at this point in the history
* Add page info field with dummy data to conneciton object

* query page info

* Enable metadata feature for torii

* Add page info field to connection object

* Fix page info query

* [WIP] Nullable page info

* Return page info even for limit/offset based pagination
  • Loading branch information
JunichiSugiura authored Nov 19, 2023
1 parent 62980bb commit 9996919
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 19 deletions.
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

0 comments on commit 9996919

Please sign in to comment.