Skip to content

Commit

Permalink
Add optional limit/offset to plural graphql queries (#1007)
Browse files Browse the repository at this point in the history
* Refactor/fix graphql pagination logic

* Consolidate singluar and plural queries

* Fix graphql entities test

* Add optional limit/offset to plural graphql queries

* Offset pagination test
  • Loading branch information
broody authored Oct 12, 2023
1 parent ce4bca6 commit d416d4b
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 34 deletions.
38 changes: 20 additions & 18 deletions crates/torii/graphql/src/object/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ use super::ObjectTrait;
use crate::query::order::Order;
use crate::query::value_mapping_from_row;
use crate::types::{GraphqlType, TypeData, TypeMapping, ValueMapping};
use crate::utils::parse_argument::ParseArgument;

pub mod cursor;
pub mod edge;
pub mod page_info;

#[derive(Debug)]
pub struct ConnectionArguments {
pub first: Option<i64>,
pub last: Option<i64>,
pub first: Option<u64>,
pub last: Option<u64>,
pub after: Option<String>,
pub before: Option<String>,
pub offset: Option<u64>,
pub limit: Option<u64>,
}

pub struct ConnectionObject {
Expand Down Expand Up @@ -59,11 +62,12 @@ impl ObjectTrait for ConnectionObject {
}

pub fn parse_connection_arguments(ctx: &ResolverContext<'_>) -> Result<ConnectionArguments, Error> {
let first = ctx.args.try_get("first").and_then(|first| first.i64()).ok();
let last = ctx.args.try_get("last").and_then(|last| last.i64()).ok();
let after = ctx.args.try_get("after").and_then(|after| Ok(after.string()?.to_string())).ok();
let before =
ctx.args.try_get("before").and_then(|before| Ok(before.string()?.to_string())).ok();
let first: Option<u64> = ParseArgument::parse(ctx, "first").ok();
let last: Option<u64> = ParseArgument::parse(ctx, "last").ok();
let after: Option<String> = ParseArgument::parse(ctx, "after").ok();
let before: Option<String> = ParseArgument::parse(ctx, "before").ok();
let offset: Option<u64> = ParseArgument::parse(ctx, "offset").ok();
let limit: Option<u64> = ParseArgument::parse(ctx, "limit").ok();

if first.is_some() && last.is_some() {
return Err(
Expand All @@ -77,19 +81,15 @@ pub fn parse_connection_arguments(ctx: &ResolverContext<'_>) -> Result<Connectio
);
}

if let Some(first) = first {
if first < 0 {
return Err("`first` on a connection cannot be less than zero.".into());
}
}

if let Some(last) = last {
if last < 0 {
return Err("`last` on a connection cannot be less than zero.".into());
}
if (offset.is_some() || limit.is_some())
&& (first.is_some() || last.is_some() || before.is_some() || after.is_some())
{
return Err("Pass either `offset`/`limit` OR `first`/`last`/`after`/`before` to paginate \
a connection."
.into());
}

Ok(ConnectionArguments { first, last, after, before })
Ok(ConnectionArguments { first, last, after, before, offset, limit })
}

pub fn connection_arguments(field: Field) -> Field {
Expand All @@ -98,6 +98,8 @@ pub fn connection_arguments(field: Field) -> Field {
.argument(InputValue::new("last", TypeRef::named(TypeRef::INT)))
.argument(InputValue::new("before", TypeRef::named(GraphqlType::Cursor.to_string())))
.argument(InputValue::new("after", TypeRef::named(GraphqlType::Cursor.to_string())))
.argument(InputValue::new("offset", TypeRef::named(TypeRef::INT)))
.argument(InputValue::new("limit", TypeRef::named(TypeRef::INT)))
}

pub fn connection_output(
Expand Down
2 changes: 1 addition & 1 deletion crates/torii/graphql/src/query/constants.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub const DEFAULT_LIMIT: i64 = 10;
pub const DEFAULT_LIMIT: u64 = 10;
pub const BOOLEAN_TRUE: i64 = 1;

pub const ENTITY_TABLE: &str = "entities";
Expand Down
7 changes: 6 additions & 1 deletion crates/torii/graphql/src/query/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ 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);

// NOTE: Order is determined by the `order` param if provided, otherwise it's inferred from the
// `first` or `last` param. Explicit ordering take precedence
let limit = connection.first.or(connection.last).unwrap_or(DEFAULT_LIMIT);
match order {
Some(order) => {
let column_name = format!("external_{}", order.field);
Expand All @@ -111,6 +112,10 @@ pub async fn fetch_multiple_rows(
}
};

if let Some(offset) = connection.offset {
query.push_str(&format!(" OFFSET {}", offset));
}

sqlx::query(&query).fetch_all(conn).await
}

Expand Down
37 changes: 31 additions & 6 deletions crates/torii/graphql/src/tests/entities_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ mod tests {
use torii_core::sql::Sql;

use crate::tests::{
entity_fixtures, paginate, run_graphql_query, Entity, Moves, Paginate, Position,
cursor_paginate, entity_fixtures, offset_paginate, run_graphql_query, Entity, Moves,
Paginate, Position,
};

#[sqlx::test(migrations = "../migrations")]
Expand Down Expand Up @@ -77,35 +78,59 @@ mod tests {
}

#[sqlx::test(migrations = "../migrations")]
async fn test_entities_pagination(pool: SqlitePool) {
async fn test_entities_cursor_pagination(pool: SqlitePool) {
let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap();
entity_fixtures(&mut db).await;

let page_size = 2;

// Forward pagination
let entities_connection = paginate(&pool, None, Paginate::Forward, page_size).await;
let entities_connection = cursor_paginate(&pool, None, Paginate::Forward, page_size).await;
assert_eq!(entities_connection.total_count, 3);
assert_eq!(entities_connection.edges.len(), page_size);

let cursor: String = entities_connection.edges[0].cursor.clone();
let next_cursor: String = entities_connection.edges[1].cursor.clone();
let entities_connection = paginate(&pool, Some(cursor), Paginate::Forward, page_size).await;
let entities_connection =
cursor_paginate(&pool, Some(cursor), Paginate::Forward, page_size).await;
assert_eq!(entities_connection.total_count, 3);
assert_eq!(entities_connection.edges.len(), page_size);
assert_eq!(entities_connection.edges[0].cursor, next_cursor);

// Backward pagination
let entities_connection = paginate(&pool, None, Paginate::Backward, page_size).await;
let entities_connection = cursor_paginate(&pool, None, Paginate::Backward, page_size).await;
assert_eq!(entities_connection.total_count, 3);
assert_eq!(entities_connection.edges.len(), page_size);

let cursor: String = entities_connection.edges[0].cursor.clone();
let next_cursor: String = entities_connection.edges[1].cursor.clone();
let entities_connection =
paginate(&pool, Some(cursor), Paginate::Backward, page_size).await;
cursor_paginate(&pool, Some(cursor), Paginate::Backward, page_size).await;
assert_eq!(entities_connection.total_count, 3);
assert_eq!(entities_connection.edges.len(), page_size);
assert_eq!(entities_connection.edges[0].cursor, next_cursor);
}

#[sqlx::test(migrations = "../migrations")]
async fn test_entities_offset_pagination(pool: SqlitePool) {
let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap();
entity_fixtures(&mut db).await;

let limit = 3;
let mut offset = 0;
let entities_connection = offset_paginate(&pool, offset, limit).await;
let offset_plus_one = entities_connection.edges[1].node.model_names.clone();
let offset_plus_two = entities_connection.edges[2].node.model_names.clone();
assert_eq!(entities_connection.edges.len(), 3);

offset = 1;
let entities_connection = offset_paginate(&pool, offset, limit).await;
assert_eq!(entities_connection.edges[0].node.model_names, offset_plus_one);
assert_eq!(entities_connection.edges.len(), 2);

offset = 2;
let entities_connection = offset_paginate(&pool, offset, limit).await;
assert_eq!(entities_connection.edges[0].node.model_names, offset_plus_two);
assert_eq!(entities_connection.edges.len(), 1);
}
}
25 changes: 24 additions & 1 deletion crates/torii/graphql/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ pub async fn entity_fixtures(db: &mut Sql) {
db.execute().await.unwrap();
}

pub async fn paginate(
pub async fn cursor_paginate(
pool: &SqlitePool,
cursor: Option<String>,
direction: Paginate,
Expand Down Expand Up @@ -334,3 +334,26 @@ pub async fn paginate(
let entities = value.get("entities").ok_or("entities not found").unwrap();
serde_json::from_value(entities.clone()).unwrap()
}

pub async fn offset_paginate(pool: &SqlitePool, offset: u64, limit: u64) -> Connection<Entity> {
let query = format!(
"
{{
entities (offset: {offset}, limit: {limit})
{{
total_count
edges {{
cursor
node {{
model_names
}}
}}
}}
}}
"
);

let value = run_graphql_query(pool, &query).await;
let entities = value.get("entities").ok_or("entities not found").unwrap();
serde_json::from_value(entities.clone()).unwrap()
}
3 changes: 1 addition & 2 deletions crates/torii/graphql/src/tests/types-test/src/systems.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait};
use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
Expand All @@ -18,7 +17,7 @@ mod records {
fn create(self: @ContractState, num_records: u8) {
let world = self.world_dispatcher.read();
let mut record_idx = 0;

loop {
if record_idx == num_records {
break ();
Expand Down
10 changes: 5 additions & 5 deletions crates/torii/graphql/src/utils/parse_argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ use async_graphql::dynamic::ResolverContext;
use async_graphql::Result;

pub trait ParseArgument: Sized {
fn parse(ctx: &ResolverContext<'_>, input: String) -> Result<Self>;
fn parse(ctx: &ResolverContext<'_>, input: &str) -> Result<Self>;
}

impl ParseArgument for u64 {
fn parse(ctx: &ResolverContext<'_>, input: String) -> Result<Self> {
let arg = ctx.args.try_get(input.as_str());
fn parse(ctx: &ResolverContext<'_>, input: &str) -> Result<Self> {
let arg = ctx.args.try_get(input);
arg?.u64()
}
}

impl ParseArgument for String {
fn parse(ctx: &ResolverContext<'_>, input: String) -> Result<Self> {
let arg = ctx.args.try_get(input.as_str());
fn parse(ctx: &ResolverContext<'_>, input: &str) -> Result<Self> {
let arg = ctx.args.try_get(input);
Ok(arg?.string()?.to_string())
}
}

0 comments on commit d416d4b

Please sign in to comment.