Skip to content

Creating a generic GraphQL structure #1051

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

Closed
ClementNerma opened this issue Mar 31, 2022 · 6 comments
Closed

Creating a generic GraphQL structure #1051

ClementNerma opened this issue Mar 31, 2022 · 6 comments
Assignees
Labels
Milestone

Comments

@ClementNerma
Copy link

Hi there!

I'm currently writing a Paginated<T> type for paginated responses, where T is a value that can be handled by GraphQL.

I struggle at writing this structure, here is what I have currently:

struct Paginated<T> {
  items: Vec<T>
}

#[graphql_object]
impl<T> Paginated<T> {
  pub fn items(&self) -> &[T] {
    self.items.as_slice()
  }
}

This fails because T is not a GraphQLValue. Seems logical, so I added a where T: GraphQLValue<DefaultScalarValue> constraint to my impl. But now it asks me to also implement IsOutputType, GraphQLType, GraphQLValueAsync, ensure that <T as GraphQLValue>::Context: juniper::Context, and so on.

This is a lot of constraints (especially the GraphQLValueAsync part) so is there a simpler way to constraint T?

Thanks in advance for your help!

@tyranron tyranron self-assigned this Mar 31, 2022
@tyranron
Copy link
Member

@ClementNerma unfortunately, you cannot do things like this conceptually:

#[graphql_object]
impl<T> Paginated<T> {

The problem is that #[graphql_object] generates always a single GraphQL object in schema. So, even if you do so, Paginated<User> and Paginated<Message> will correspond to a single GraphQL paginated type, which is incorrect (the same field of the same GraphQL type cannot produce diferent GraphQL types statically).

In our codebases we solve this problem in the following way:

  1. Declare generic implementation as much as possible:
/// Generic [Connection Type][1] according to
/// [GraphQL Cursor Connections Specification][0].
///
/// [0]: https://tinyurl.com/gql-relay
/// [1]: https://tinyurl.com/gql-relay#sec-Connection-Types
#[derive(Debug, SmartDefault)]
pub struct Connection<Node, Cursor> {
    /// List of [`Edge`]s for this [`Connection`].
    pub edges: Vec<Edge<Node, Cursor>>,

    /// Indicates whether this [`Connection`] has next page.
    pub has_next_page: bool,

    /// Indicates whether this [`Connection`] has previous page.
    pub has_previous_page: bool,
}

impl<Node, Cursor> Connection<Node, Cursor> {
    /// Returns list of [`Edge`]s for this [`Connection`].
    pub(crate) fn _edges(&self) -> &[Edge<Node, Cursor>] {
        self.edges.as_slice()
    }

    /// Returns list of this [`Connection`] `Node`s.
    pub(crate) fn _nodes(&self) -> Vec<&Node> {
        self.edges.iter().map(|edge| &edge.node).collect()
    }

    /// Returns [`PageInfo`] of this [`Connection`].
    pub(crate) fn _page_info(&self) -> PageInfo<'_>
    where
        Cursor: AsRef<str>,
    {
        PageInfo {
            end_cursor: self.edges.last().map(|edge| edge.cursor.as_ref()),
            has_next_page: self.has_next_page,
            start_cursor: self.edges.first().map(|edge| edge.cursor.as_ref()),
            has_previous_page: self.has_previous_page,
        }
    }
}

/// Generic [Edge Type] according to
/// [GraphQL Cursor Connections Specification][0].
///
/// [0]: https://tinyurl.com/gql-relay
/// [Edge Type]: https://tinyurl.com/gql-relay#sec-Edge-Types
#[derive(Clone, Debug)]
pub struct Edge<Node, Cursor> {
    /// Item at the end of this [`Edge`].
    pub node: Node,

    /// Cursor of this [`Edge`].
    pub cursor: Cursor,
}

/// [`PageInfo`][1] returned by a [Connection] according to
/// [GraphQL Cursor Connections Specification][0].
///
/// [0]: https://tinyurl.com/gql-relay
/// [1]: https://tinyurl.com/gql-relay#sec-Connection-Types.Fields.PageInfo
/// [Connection]: https://tinyurl.com/gql-relay#sec-Connection-Types
#[derive(Debug, GraphQLObject)]
pub struct PageInfo<'a> {
    /// [Cursor] pointing to the last [Node] in [Connection]'s [Edges].
    ///
    /// [Cursor]: https://tinyurl.com/gql-relay#sec-Cursor
    /// [Node]: https://tinyurl.com/gql-relay#sec-Node
    /// [Connection]: https://tinyurl.com/gql-relay#sec-Connection-Types
    /// [Edges]: https://tinyurl.com/gql-relay#sec-Edges
    pub end_cursor: Option<&'a str>,

    /// Indicator whether more [Edge]s exist following the set defined by the
    /// clients arguments.
    ///
    /// If the client is paginating with `first`/`after`, then `true` is
    /// returned if further [Edge]s exist, otherwise `false`.
    ///
    /// If the client is paginating with `last`/`before`, then `false` is
    /// returned.
    ///
    /// See [`PageInfo` fields spec][1] for more details.
    ///
    /// [1]: https://tinyurl.com/gql-relay#sec-undefined.PageInfo.Fields
    /// [Edge]: https://tinyurl.com/gql-relay#sec-Edge-Types
    pub has_next_page: bool,

    /// [Cursor] pointing to the first [Node] in [Connection]'s [Edges].
    ///
    /// [Cursor]: https://tinyurl.com/gql-relay#sec-Cursor
    /// [Node]: https://tinyurl.com/gql-relay#sec-Node
    /// [Connection]: https://tinyurl.com/gql-relay#sec-Connection-Types
    /// [Edges]: https://tinyurl.com/gql-relay#sec-Edges
    pub start_cursor: Option<&'a str>,

    /// Indicator whether more [Edge]s exist prior to the set defined by the
    /// clients arguments.
    ///
    /// If the client is paginating with `last`/`before`, then `true` is
    /// returned if prior [Edge]s exist, otherwise `false`.
    ///
    /// If the client is paginating with `first`/`after`, then `false` is
    /// returned.
    ///
    /// See [`PageInfo` fields spec][1] for more details.
    ///
    /// [1]: https://tinyurl.com/gql-relay#sec-undefined.PageInfo.Fields
    /// [Edge]: https://tinyurl.com/gql-relay#sec-Edge-Types
    pub has_previous_page: bool,
}
  1. Define concrete pagination GraphQL types for each paginated GraphQL type separately:
/// [`Connection`] with [`User`]s.
pub type UsersConnection = Connection<User, UsersCursor>;

/// [Connection] with `User`s.
///
/// [Connection]: https://tinyurl.com/gql-relay#sec-Connection-Types
#[graphql_object(context = Context)]
impl UsersConnection {
    /// List of `User` [Edges] in this [Connection].
    ///
    /// [Connection]: https://tinyurl.com/gql-relay#sec-Connection-Types
    /// [Edges]: https://tinyurl.com/gql-relay#sel-FAFFFBEAAAACBFpwI
    pub fn edges(&self) -> &[UsersEdge] {
        self._edges()
    }

    /// List of `User`s in this [Connection].
    ///
    /// [Connection]: https://tinyurl.com/gql-relay#sec-Connection-Types
    pub fn nodes(&self) -> Vec<&User> {
        self._nodes()
    }

    /// [PageInfo] of this [Connection].
    ///
    /// [Connection]: https://tinyurl.com/gql-relay#sec-Connection-Types
    /// [PageInfo]: https://tinyurl.com/gql-relay#sec-undefined.PageInfo
    pub fn page_info(&self) -> PageInfo<'_> {
        self._page_info()
    }
}

/// [`Connection`]'s [`Edge`] with an [`User`].
pub type UsersEdge = Edge<User, UsersCursor>;

/// [Edge] with an `User`.
///
/// [Edge]: https://tinyurl.com/gql-relay#sec-Edge-Types
#[graphql_object(context = Context)]
impl UsersEdge {
    /// `User` [Node] at the end of this [Edge].
    ///
    /// [Edge]: https://tinyurl.com/gql-relay#sec-Edge-Types
    /// [Node]: https://tinyurl.com/gql-relay#sec-Node
    pub fn node(&self) -> &User {
        &self.node
    }

    /// [Cursor] of this [Edge].
    ///
    /// [Cursor]: https://tinyurl.com/gql-relay#sec-Cursor
    /// [Edge]: https://tinyurl.com/gql-relay#sec-Edge-Types
    pub fn cursor(&self) -> &UsersCursor {
        &self.cursor
    }
}

@ClementNerma
Copy link
Author

Thanks for your answer!

I see, so there needs to be one concrete type per "pagination" type, is that right?

I also tried that, this way:

#[macro_export]
macro_rules! declare_paginable {
    ($type: ident as $alias: ident) => {
        pub struct $alias {
            pub items: Vec<$type>,
            pub from: i32,
            pub has_more: bool,
            pub total: i32,
        }

        #[juniper::graphql_object]
        impl $alias {
            pub fn items(&self) -> &[$type] {
                self.items.as_slice()
            }

            pub fn from(&self) -> i32 {
                self.from
            }

            pub fn has_more(&self) -> bool {
                self.has_more
            }

            pub fn total(&self) -> i32 {
                self.total
            }
        }
    };
}

But then, when I use it:

#[derive(Clone)]
struct Test {
    a: String,
}

crate::declare_paginable!(Test as PaginatedTest);

I get the following error: could not determine a name for the impl type, as the procedural macro #[graphql_object] is evaluated before the $alias argument is resolved.

Do you have an idea on how to solve this? I have a lot of type so I cannot afford to write a manual implementation by hand for each of them, which is why I'm trying to automate it.

@tyranron
Copy link
Member

@ClementNerma

I see, so there needs to be one concrete type per "pagination" type, is that right?

Yes.

I get the following error: could not determine a name for the impl type, as the procedural macro #[graphql_object] is evaluated before the $alias argument is resolved.

Wow... never met this kind of an error 🤔 I guess because we've never used juniper proc macros in combination with declarative ones.

Googling on this problem says that you shoud consider eager crate.

@ClementNerma
Copy link
Author

Ok I'll look into that, thank you!

@ClementNerma
Copy link
Author

I posted some questions on Rust's forum and for the macro part, it is possible to solve it by using a tt instead of an ident in the macro. That doesn't solve the problem of the types needing to be unique, but I post it here in case it can help someone with this.

@tyranron tyranron closed this as completed Apr 1, 2022
@ClementNerma
Copy link
Author

On the macro part, the ident should probably work once this PR is merged: #1054

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants