Skip to content

Commit

Permalink
feat: support relay-style pagination, apply on uses & invitations (#1106
Browse files Browse the repository at this point in the history
)

* feat: support relay-style pagination, apply on uses & invitations

* resolve comment
  • Loading branch information
darknight authored Dec 25, 2023
1 parent 3f779fa commit 7876f57
Show file tree
Hide file tree
Showing 14 changed files with 984 additions and 30 deletions.
16 changes: 8 additions & 8 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/juniper-axum/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod extract;
pub mod relay;
pub mod response;

use std::future;
Expand Down
155 changes: 155 additions & 0 deletions crates/juniper-axum/src/relay/connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use juniper::{
marker::IsOutputType, meta::MetaType, Arguments, ExecutionResult, Executor, GraphQLType,
GraphQLValue, GraphQLValueAsync, Registry, ScalarValue,
};

use crate::relay::{edge::Edge, page_info::PageInfo, NodeType};

/// Connection type
///
/// Connection is the result of a query for `relay::query` or `relay::query_async`
pub struct Connection<Node> {
/// All edges of the current page.
pub edges: Vec<Edge<Node>>,
pub page_info: PageInfo,
}

impl<Node> Connection<Node>
where
Node: NodeType,
{
/// Returns a relay relay with no elements.
pub fn empty() -> Self {
Self {
edges: Vec::new(),
page_info: PageInfo::default(),
}
}

pub fn build_connection(nodes: Vec<Node>, first: Option<usize>, last: Option<usize>) -> Self {
let has_next_page = first.map_or(false, |first| nodes.len() > first);
let has_previous_page = last.map_or(false, |last| nodes.len() > last);
let len = nodes.len();

let edges: Vec<_> = if let Some(last) = last {
nodes
.into_iter()
.rev()
.take(last)
.rev()
.map(|node| {
let cursor = node.cursor();
Edge::new(cursor.to_string(), node)
})
.collect()
} else {
nodes
.into_iter()
.take(first.unwrap_or(len))
.map(|node| {
let cursor = node.cursor();
Edge::new(cursor.to_string(), node)
})
.collect()
};

Connection {
page_info: PageInfo {
has_previous_page,
has_next_page,
start_cursor: edges.first().map(|edge| edge.cursor.clone()),
end_cursor: edges.last().map(|edge| edge.cursor.clone()),
},
edges,
}
}
}

impl<Node, S> GraphQLType<S> for Connection<Node>
where
Node: NodeType + GraphQLType<S>,
Node::Context: juniper::Context,
S: ScalarValue,
{
fn name(_info: &Self::TypeInfo) -> Option<&str> {
Some(Node::connection_type_name())
}

fn meta<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S>
where
S: 'r,
{
let fields = [
registry.field::<&Vec<Edge<Node>>>("edges", info),
registry.field::<&PageInfo>("pageInfo", &()),
];
registry
.build_object_type::<Self>(info, &fields)
.into_meta()
}
}

impl<Node, S> GraphQLValue<S> for Connection<Node>
where
Node: NodeType + GraphQLType<S>,
Node::Context: juniper::Context,
S: ScalarValue,
{
type Context = Node::Context;
type TypeInfo = <Node as GraphQLValue<S>>::TypeInfo;

fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
<Self as GraphQLType<S>>::name(info)
}

fn resolve_field(
&self,
info: &Self::TypeInfo,
field_name: &str,
_arguments: &Arguments<S>,
executor: &Executor<Self::Context, S>,
) -> ExecutionResult<S> {
match field_name {
"edges" => executor.resolve_with_ctx(info, &self.edges),
"pageInfo" => executor.resolve_with_ctx(&(), &self.page_info),
_ => panic!("Field {} not found on type ConnectionEdge", field_name),
}
}

fn concrete_type_name(&self, _context: &Self::Context, info: &Self::TypeInfo) -> String {
self.type_name(info).unwrap_or("Connection").to_string()
}
}

impl<Node, S> GraphQLValueAsync<S> for Connection<Node>
where
Node: NodeType + GraphQLType<S> + GraphQLValueAsync<S> + Send + Sync,
Node::TypeInfo: Sync,
Node::Context: juniper::Context + Sync,
S: ScalarValue + Send + Sync,
{
fn resolve_field_async<'a>(
&'a self,
info: &'a Self::TypeInfo,
field_name: &'a str,
_arguments: &'a Arguments<S>,
executor: &'a Executor<Self::Context, S>,
) -> juniper::BoxFuture<'a, ExecutionResult<S>> {
let f = async move {
match field_name {
"edges" => executor.resolve_with_ctx_async(info, &self.edges).await,
"pageInfo" => executor.resolve_with_ctx(&(), &self.page_info),
_ => panic!("Field {} not found on type ConnectionEdge", field_name),
}
};
use ::juniper::futures::future;
future::FutureExt::boxed(f)
}
}

impl<Node, S> IsOutputType<S> for Connection<Node>
where
Node: GraphQLType<S>,
S: ScalarValue,
{
}
109 changes: 109 additions & 0 deletions crates/juniper-axum/src/relay/edge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use juniper::{
marker::IsOutputType, meta::MetaType, Arguments, BoxFuture, ExecutionResult, Executor,
GraphQLType, GraphQLValue, GraphQLValueAsync, Registry, ScalarValue,
};

use crate::relay::NodeType;

/// An edge in a relay.
pub struct Edge<Node> {
pub cursor: String,
pub node: Node,
}

impl<Node> Edge<Node> {
/// Create a new edge.
#[inline]
pub fn new(cursor: String, node: Node) -> Self {
Self { cursor, node }
}
}

impl<Node, S> GraphQLType<S> for Edge<Node>
where
Node: NodeType + GraphQLType<S>,
Node::Context: juniper::Context,
S: ScalarValue,
{
fn name(_info: &Self::TypeInfo) -> Option<&str> {
Some(Node::edge_type_name())
}

fn meta<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S>
where
S: 'r,
{
let fields = [
registry.field::<&Node>("node", info),
registry.field::<&String>("cursor", &()),
];
registry
.build_object_type::<Self>(info, &fields)
.into_meta()
}
}

impl<Node, S> GraphQLValue<S> for Edge<Node>
where
Node: NodeType + GraphQLType<S>,
Node::Context: juniper::Context,
S: ScalarValue,
{
type Context = Node::Context;
type TypeInfo = <Node as GraphQLValue<S>>::TypeInfo;

fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
<Self as GraphQLType<S>>::name(info)
}

fn resolve_field(
&self,
info: &Self::TypeInfo,
field_name: &str,
_arguments: &Arguments<S>,
executor: &Executor<Self::Context, S>,
) -> ExecutionResult<S> {
match field_name {
"node" => executor.resolve_with_ctx(info, &self.node),
"cursor" => executor.resolve_with_ctx(&(), &self.cursor),
_ => panic!("Field {} not found on type ConnectionEdge", field_name),
}
}

fn concrete_type_name(&self, _context: &Self::Context, info: &Self::TypeInfo) -> String {
self.type_name(info).unwrap_or("ConnectionEdge").to_string()
}
}

impl<Node, S> GraphQLValueAsync<S> for Edge<Node>
where
Node: NodeType + GraphQLType<S> + GraphQLValueAsync<S> + Send + Sync,
Node::TypeInfo: Sync,
Node::Context: juniper::Context + Sync,
S: ScalarValue + Send + Sync,
{
fn resolve_field_async<'a>(
&'a self,
info: &'a Self::TypeInfo,
field_name: &'a str,
_arguments: &'a Arguments<S>,
executor: &'a Executor<Self::Context, S>,
) -> BoxFuture<'a, ExecutionResult<S>> {
let f = async move {
match field_name {
"node" => executor.resolve_with_ctx_async(info, &self.node).await,
"cursor" => executor.resolve_with_ctx(&(), &self.cursor),
_ => panic!("Field {} not found on type RelayConnectionEdge", field_name),
}
};
use ::juniper::futures::future;
future::FutureExt::boxed(f)
}
}

impl<Node, S> IsOutputType<S> for Edge<Node>
where
Node: GraphQLType<S>,
S: ScalarValue,
{
}
Loading

0 comments on commit 7876f57

Please sign in to comment.