Skip to content

Commit

Permalink
Add extensions for dynamic GraphQL type definition
Browse files Browse the repository at this point in the history
  • Loading branch information
AlicanC committed Aug 14, 2023
1 parent dc2a1b6 commit bf3a637
Show file tree
Hide file tree
Showing 11 changed files with 680 additions and 1 deletion.
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/fuel-indexer-graphql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ description = "Fuel Indexer GraphQL"
async-graphql = { version = "5.0", features = ["dynamic-schema"] }
async-graphql-parser = "5.0"
async-graphql-value = "5.0"
extension-trait = "1.0.2"
fuel-indexer-database = { workspace = true }
fuel-indexer-database-types = { workspace = true }
fuel-indexer-schema = { workspace = true, features = ["db-models"] }
Expand All @@ -22,5 +23,8 @@ serde_json = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
assert_matches = "1.5.0"
fuel-indexer-lib = { workspace = true, default-features = true }
graphql-parser = "0.4.0"
insta = "1.31.0"
pretty_assertions = "0.5.0"
1 change: 1 addition & 0 deletions packages/fuel-indexer-graphql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod arguments;
pub mod dynamic;
pub mod graphql;
pub mod queries;
pub mod util;
74 changes: 74 additions & 0 deletions packages/fuel-indexer-graphql/src/util/connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! `async_graphql::dynamic` extensions for handling GraphQL connections.
//! See: https://graphql.org/learn/pagination/#end-of-list-counts-and-connections
//! See: https://relay.dev/graphql/connections.htm#sec-Connection-Types

pub use super::{edge::*, filtering::*, node::*, ordering::*, paging::*};
use async_graphql::dynamic::{
Field, InputObject, InputValue, Object, SchemaBuilder, TypeRef,
};
use extension_trait::extension_trait;

#[extension_trait]
pub impl TypeRefConnectionExt for TypeRef {
fn connection(node_name: impl Into<String>) -> String {
format!("{}Connection", node_name.into())
}
}

#[extension_trait]
pub impl ObjectConnectionExt for Object {
fn new_connection(node_name: impl Into<String>) -> Self {
let node_name = node_name.into();
Self::new(TypeRef::connection(node_name.clone()))
.field(Field::new(
"totalCount",
TypeRef::named_nn(TypeRef::INT),
|_| unimplemented!(),
))
.field(Field::new(
"nodes",
TypeRef::named_nn_list_nn(node_name.clone()),
|_| unimplemented!(),
))
.field(Field::new(
"edges",
TypeRef::named_nn_list_nn(TypeRef::edge(node_name)),
|_| unimplemented!(),
))
.field(Field::new(
"pageInfo",
TypeRef::named_nn(TypeRef::PAGE_INFO),
|_| unimplemented!(),
))
}
}

#[extension_trait]
pub impl FieldConnectionExt for Field {
fn connection_arguments(self, node_name: impl Into<String>) -> Self {
let node_name = node_name.into();
self.filtering_arguments(node_name.clone())
.ordering_arguments(node_name)
.paging_arguments()
}
}

#[extension_trait]
pub impl SchemaBuilderConnectionExt for SchemaBuilder {
fn register_connection_types(self) -> Self {
let id_filter_input = InputObject::new_eq_filter(TypeRef::ID);
let node_filter_input = InputObject::new_filter(TypeRef::NODE).field(
InputValue::new("id", TypeRef::named(TypeRef::filter_input(TypeRef::ID))),
);
let node_order_input = InputObject::new_order(TypeRef::NODE).field(
InputValue::new("id", TypeRef::named(TypeRef::ORDER_DIRECTION)),
);
self.register_edge_types()
.register_filtering_types()
.register_ordering_types()
.register_paging_types()
.register(id_filter_input)
.register(node_filter_input)
.register(node_order_input)
}
}
39 changes: 39 additions & 0 deletions packages/fuel-indexer-graphql/src/util/edge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! `async_graphql::dynamic` extensions for handling GraphQL edges.
//! See: https://graphql.org/learn/pagination/#pagination-and-edges
//! See: https://relay.dev/graphql/connections.htm#sec-Edge-Types.Fields

pub use super::node::*;
use async_graphql::dynamic::{Field, Object, SchemaBuilder, TypeRef};
use extension_trait::extension_trait;

#[extension_trait]
pub impl TypeRefEdgeExt for TypeRef {
fn edge(node_name: impl Into<String>) -> String {
format!("{}Edge", node_name.into())
}
}

#[extension_trait]
pub impl ObjectEdgeExt for Object {
fn new_edge(node_name: impl Into<String>) -> Self {
let node_name = node_name.into();
Self::new(TypeRef::edge(node_name.clone()))
.field(Field::new(
"node",
TypeRef::named_nn(node_name),
|_| unimplemented!(),
))
.field(Field::new(
"cursor",
TypeRef::named_nn(TypeRef::STRING),
|_| unimplemented!(),
))
}
}

#[extension_trait]
pub impl SchemaBuilderEdgeExt for SchemaBuilder {
fn register_edge_types(self) -> Self {
self.register_node_types()
}
}
70 changes: 70 additions & 0 deletions packages/fuel-indexer-graphql/src/util/filtering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! `async_graphql::dynamic` extensions for handling connection filtering.

use async_graphql::dynamic::{Field, InputObject, InputValue, SchemaBuilder, TypeRef};
use extension_trait::extension_trait;

#[extension_trait]
pub impl TypeRefFilteringExt for TypeRef {
fn filter_input(value_name: impl Into<String>) -> String {
format!("{}FilterInput", value_name.into())
}
}

#[extension_trait]
pub impl InputObjectFilteringExt for InputObject {
fn new_filter(value_name: impl Into<String>) -> Self {
let name = TypeRef::filter_input(value_name);
Self::new(name).filter_fields()
}

fn new_eq_filter(value_name: impl Into<String>) -> Self {
let name = TypeRef::filter_input(value_name);
Self::new(name.clone())
.filter_fields()
.filter_eq_fields(name)
}

fn new_ord_filter(value_name: impl Into<String>) -> Self {
let name = TypeRef::filter_input(value_name);
Self::new(name.clone())
.filter_fields()
.filter_eq_fields(name.clone())
.filter_ord_fields(name)
}

fn filter_fields(self) -> Self {
let name = self.type_name().to_string();
self.field(InputValue::new("and", TypeRef::named_nn_list(name.clone())))
.field(InputValue::new("or", TypeRef::named_nn_list(name.clone())))
.field(InputValue::new("not", TypeRef::named_nn_list(name)))
}

fn filter_eq_fields(self, value_name: impl Into<String>) -> Self {
let value_name = value_name.into();
self.field(InputValue::new("eq", TypeRef::named(value_name.clone())))
.field(InputValue::new("in", TypeRef::named_nn_list(value_name)))
}

fn filter_ord_fields(self, value_name: impl Into<String>) -> Self {
let value_name = value_name.into();
self.field(InputValue::new("gt", TypeRef::named(value_name.clone())))
.field(InputValue::new("gte", TypeRef::named(value_name.clone())))
.field(InputValue::new("lt", TypeRef::named(value_name.clone())))
.field(InputValue::new("lte", TypeRef::named(value_name)))
}
}

#[extension_trait]
pub impl SchemaBuilderFilteringExt for SchemaBuilder {
fn register_filtering_types(self) -> Self {
self
}
}

#[extension_trait]
pub impl FieldFilteringExt for Field {
fn filtering_arguments(self, value_name: impl Into<String>) -> Self {
let filter_name = TypeRef::filter_input(value_name);
self.argument(InputValue::new("filter", TypeRef::named(filter_name)))
}
}
Loading

0 comments on commit bf3a637

Please sign in to comment.