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

Expose order params #1332

Merged
merged 18 commits into from
Jan 5, 2024
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
7 changes: 7 additions & 0 deletions crates/torii/graphql/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ 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";
pub const MODEL_ORDER_TYPE_NAME: &str = "World__ModelOrder";
pub const MODEL_ORDER_FIELD_TYPE_NAME: &str = "World__ModelOrderField";

// objects' single and plural names
pub const ENTITY_NAMES: (&str, &str) = ("entity", "entities");
Expand All @@ -34,3 +36,8 @@ pub const SOCIAL_NAMES: (&str, &str) = ("social", "socials");
pub const CONTENT_NAMES: (&str, &str) = ("content", "contents");
pub const METADATA_NAMES: (&str, &str) = ("metadata", "metadatas");
pub const TRANSACTION_NAMES: (&str, &str) = ("transaction", "transactions");

// misc
pub const ORDER_DIR_TYPE_NAME: &str = "OrderDirection";
pub const ORDER_ASC: &str = "ASC";
pub const ORDER_DESC: &str = "DESC";
9 changes: 7 additions & 2 deletions crates/torii/graphql/src/object/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,15 @@ pub fn connection_output(
.iter()
.map(|row| {
let order_field = match order {
Some(order) => format!("external_{}", order.field),
Some(order) => {
if is_external {
format!("external_{}", order.field)
} else {
order.field.to_string()
}
}
None => id_column.to_string(),
};

let primary_order = row.try_get::<String, &str>(id_column)?;
let secondary_order = row.try_get_unchecked::<String, &str>(&order_field)?;
let cursor = cursor::encode(&primary_order, &secondary_order);
Expand Down
6 changes: 3 additions & 3 deletions crates/torii/graphql/src/object/inputs/order_input.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use async_graphql::dynamic::{Enum, Field, InputObject, InputValue, ResolverContext, TypeRef};

use super::InputObjectTrait;
use crate::constants::{ORDER_ASC, ORDER_DESC, ORDER_DIR_TYPE_NAME};
use crate::object::TypeMapping;
use crate::query::order::{Direction, Order};

Expand All @@ -27,7 +28,7 @@ impl InputObjectTrait for OrderInputObject {
fn input_object(&self) -> InputObject {
// direction and field values are required (not null)
InputObject::new(self.type_name())
.field(InputValue::new("direction", TypeRef::named_nn("OrderDirection")))
.field(InputValue::new("direction", TypeRef::named_nn(ORDER_DIR_TYPE_NAME)))
.field(InputValue::new(
"field",
TypeRef::named_nn(format!("{}Field", self.type_name())),
Expand All @@ -36,7 +37,7 @@ impl InputObjectTrait for OrderInputObject {

fn enum_objects(&self) -> Option<Vec<Enum>> {
// Direction enum has only two members ASC and DESC
let direction = Enum::new("OrderDirection").item("ASC").item("DESC");
let direction = Enum::new(ORDER_DIR_TYPE_NAME).item(ORDER_ASC).item(ORDER_DESC);

// Field Order enum consist of all members of a model
let field_order = self
Expand All @@ -45,7 +46,6 @@ impl InputObjectTrait for OrderInputObject {
.fold(Enum::new(format!("{}Field", self.type_name())), |acc, (ty_name, _)| {
acc.item(ty_name.to_uppercase())
});

Some(vec![direction, field_order])
}
}
Expand Down
1 change: 0 additions & 1 deletion crates/torii/graphql/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ pub trait ObjectTrait: Send + Sync {
object = object.field(field);
}
}

vec![object]
}
}
81 changes: 79 additions & 2 deletions crates/torii/graphql/src/object/model.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
use async_graphql::dynamic::indexmap::IndexMap;
use async_graphql::dynamic::{InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef};
use async_graphql::dynamic::{
Enum, Field, FieldFuture, InputObject, InputValue, SubscriptionField, SubscriptionFieldFuture,
TypeRef,
};
use async_graphql::{Name, Value};
use sqlx::{Pool, Sqlite};
use tokio_stream::StreamExt;
use torii_core::simple_broker::SimpleBroker;
use torii_core::types::Model;

use super::connection::{connection_arguments, connection_output, parse_connection_arguments};
use super::inputs::order_input::parse_order_argument;
use super::{ObjectTrait, TypeMapping, ValueMapping};
use crate::constants::{MODEL_NAMES, MODEL_TABLE, MODEL_TYPE_NAME};
use crate::constants::{
ID_COLUMN, MODEL_NAMES, MODEL_ORDER_FIELD_TYPE_NAME, MODEL_ORDER_TYPE_NAME, MODEL_TABLE,
MODEL_TYPE_NAME, ORDER_ASC, ORDER_DESC, ORDER_DIR_TYPE_NAME,
};
use crate::mapping::MODEL_TYPE_MAPPING;
use crate::query::data::{count_rows, fetch_multiple_rows};

const ORDER_BY_NAME: &str = "NAME";
const ORDER_BY_HASH: &str = "CLASS_HASH";

pub struct ModelObject;

Expand All @@ -28,6 +41,70 @@ impl ObjectTrait for ModelObject {
Some(MODEL_TABLE)
}

fn input_objects(&self) -> Option<Vec<InputObject>> {
let order_input = InputObject::new(MODEL_ORDER_TYPE_NAME)
.field(InputValue::new("direction", TypeRef::named_nn(ORDER_DIR_TYPE_NAME)))
.field(InputValue::new("field", TypeRef::named_nn(MODEL_ORDER_FIELD_TYPE_NAME)));

Some(vec![order_input])
}

fn enum_objects(&self) -> Option<Vec<Enum>> {
let direction = Enum::new(ORDER_DIR_TYPE_NAME).item(ORDER_ASC).item(ORDER_DESC);
let field_order =
Enum::new(MODEL_ORDER_FIELD_TYPE_NAME).item(ORDER_BY_NAME).item(ORDER_BY_HASH);

Some(vec![direction, field_order])
}

fn resolve_many(&self) -> Option<Field> {
let type_mapping = self.type_mapping().clone();
let table_name = self.table_name().unwrap().to_string();

let mut field = Field::new(
self.name().1,
TypeRef::named(format!("{}Connection", self.type_name())),
move |ctx| {
let type_mapping = type_mapping.clone();
let table_name = table_name.to_string();

FieldFuture::new(async move {
let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
let order = parse_order_argument(&ctx);
let connection = parse_connection_arguments(&ctx)?;
let total_count = count_rows(&mut conn, &table_name, &None, &None).await?;
let (data, page_info) = fetch_multiple_rows(
&mut conn,
&table_name,
ID_COLUMN,
&None,
&order,
&None,
&connection,
total_count,
)
.await?;
let results = connection_output(
&data,
&type_mapping,
&order,
ID_COLUMN,
total_count,
false,
page_info,
)?;

Ok(Some(Value::Object(results)))
})
},
);

field = connection_arguments(field);
field = field.argument(InputValue::new("order", TypeRef::named(MODEL_ORDER_TYPE_NAME)));

Some(field)
}

fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
Some(vec![
SubscriptionField::new("modelRegistered", TypeRef::named_nn(self.type_name()), |ctx| {
Expand Down
8 changes: 5 additions & 3 deletions crates/torii/graphql/src/query/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use sqlx::{Result, Row, SqliteConnection};

use super::filter::{Filter, FilterValue};
use super::order::{CursorDirection, Direction, Order};
use crate::constants::DEFAULT_LIMIT;
use crate::constants::{DEFAULT_LIMIT, MODEL_TABLE};
use crate::object::connection::{cursor, ConnectionArguments};

pub async fn count_rows(
Expand Down Expand Up @@ -85,7 +85,10 @@ pub async fn fetch_multiple_rows(
// `first` or `last` param. Explicit ordering take precedence
match order {
Some(order) => {
let column_name = format!("external_{}", order.field);
let mut column_name = order.field.clone();
if table_name != MODEL_TABLE {
column_name = format!("external_{}", column_name);
}
query.push_str(&format!(
" ORDER BY {column_name} {}, {id_column} {} LIMIT {limit}",
order.direction.as_ref(),
Expand Down Expand Up @@ -125,7 +128,6 @@ pub async fn fetch_multiple_rows(
Some(order) => format!("external_{}", order.field),
None => id_column.to_string(),
};

match cursor_param {
Some(cursor_query) => {
let first_cursor = cursor::encode(
Expand Down
11 changes: 11 additions & 0 deletions crates/torii/graphql/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use torii_core::sql::Sql;

mod entities_test;
mod metadata_test;
mod models_ordering_test;
mod models_test;
mod subscription_test;

Expand Down Expand Up @@ -69,6 +70,16 @@ pub struct PageInfo {
pub end_cursor: Option<String>,
}

#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct WorldModel {
pub id: String,
pub name: String,
pub class_hash: String,
pub transaction_hash: String,
pub created_at: String,
}

#[derive(Deserialize, Debug, PartialEq)]
pub struct Record {
pub __typename: String,
Expand Down
74 changes: 74 additions & 0 deletions crates/torii/graphql/src/tests/models_ordering_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#[cfg(test)]
mod tests {
use anyhow::Result;
use async_graphql::dynamic::Schema;
use serde_json::Value;

use crate::schema::build_schema;
use crate::tests::{run_graphql_query, spinup_types_test, Connection, WorldModel};

async fn world_model_query(schema: &Schema, arg: &str) -> Value {
let query = format!(
r#"
{{
models {} {{
totalCount
edges {{
cursor
node {{
id
name
classHash
transactionHash
createdAt
}}
}}
pageInfo{{
startCursor
hasPreviousPage
hasNextPage
startCursor
endCursor
}}
}}
}}
"#,
arg,
);

let result = run_graphql_query(schema, &query).await;
result.get("models").ok_or("models not found").unwrap().clone()
}

// End to end test spins up a test sequencer and deploys types-test project, this takes a while
// to run so combine all related tests into one
#[tokio::test(flavor = "multi_thread")]
async fn models_ordering_test() -> Result<()> {
let pool = spinup_types_test().await?;
let schema = build_schema(&pool).await.unwrap();

// default params, test entity relationship, test nested types
let world_model = world_model_query(&schema, "").await;
let connection: Connection<WorldModel> = serde_json::from_value(world_model).unwrap();
let first_model = connection.edges.first().unwrap();
let second_model = connection.edges.get(1).unwrap();
let last_model = connection.edges.get(2).unwrap();
assert_eq!(&first_model.node.name, "Subrecord");
assert_eq!(&second_model.node.name, "RecordSibling");
assert_eq!(&last_model.node.name, "Record");

// *** ORDER TESTING ***

// order on name string ASC (number)
let world_model =
world_model_query(&schema, "(order: {field: NAME, direction: ASC})").await;
let connection: Connection<WorldModel> = serde_json::from_value(world_model).unwrap();
let first_model = connection.edges.first().unwrap();
let second_model = connection.edges.get(1).unwrap();
let last_model = connection.edges.get(2).unwrap();
assert_eq!(&first_model.node.name, "Record");
assert_eq!(&second_model.node.name, "RecordSibling");
assert_eq!(&last_model.node.name, "Subrecord");
Ok(())
}
}
Loading