GraphQLSwift / Graphiti

The Swift GraphQL Schema framework for macOS and Linux
MIT License
531 stars 67 forks source link

Is there an example of a Graphiti schema using connections? #40

Closed cshadek closed 1 year ago

cshadek commented 4 years ago

I noticed that @paulofaria added connections in an earlier commit. I was trying to use them in my code but have not succeeded in getting them working.

Here is what I have been trying. What am I missing here?

Inside the Schema Definition:

Type(User.self,
Field("id", at: \.id).description("The unique id of the user."),
Field("followersConnection", at: User.followers).description("The followers connection")
)

In another file:

class User: Codable, Identifiable {
var id: String? = UUID().uuidString

func followers(context: UserContext, arguments: ForwardPaginationArguments) -> EventLoopFuture<Connection<User>> {
        let arr = context.request.eventLoop.makeSucceededFuture([User()])
        return arr.connection(from: arguments)
    }
}

I get this error:

[ ERROR ] Cannot use type "Connection<User>" for field "followersConnection". Type does not map to a GraphQL type.

Thanks!

paulofaria commented 4 years ago

You also need to define the connection type in the schema.

ConnectionType(User.self),

I'll add tests for connections and link here soon.

cshadek commented 4 years ago

Thanks @paulofaria for getting back to me so quickly and I saw you have already started improving connections.

I tried a few things based on your suggestion.


When I add ConnectionType(User.self) before the Type definition for User.self

I get this error: Cannot use type "User" for field "node". Type does not map to a GraphQL type.


If I add ConnectionType(User.self) after the Type definition for User.self

I get this error: Cannot use type "Connection<User>" for field "followersConnection". Type does not map to a GraphQL type.


I have also tried using ConnectionType(TypeReference<User>.self) but TypeReference<User> is not encodable.

MaxDesiatov commented 3 years ago

Any updates on this? I wasn't able to find any tests or example code that show how Connection and ConnectionType can be used, or am I missing something?

paulofaria commented 3 years ago

Hey, sorry! I'll add an example this weekend.

paulofaria commented 3 years ago

Here's an example of the schema definition.

import Foundation
import Entities
import Graphiti

public protocol GraphQLContext {
    var app: GraphQLApp { get }
    var accessToken: AccessToken? { get }
}

public struct GraphQLAPI: API {
    public let resolver: GraphQLResolver
    public let schema: Schema<GraphQLResolver, GraphQLContext>

    public init(resolver: GraphQLResolver) throws {
        self.resolver = resolver

        self.schema = try Schema<GraphQLResolver, GraphQLContext> {
            DateScalar(formatter: ISO8601DateFormatter())

            Scalar(URL.self)

            Scalar(UUID.self)

            Type(AccessToken.self) {
                Field("token", at: \.token)
            }

            Type(Channel.self) {
                Field("id", at: \.id)
                Field("name", at: \.name)
            }

            Type(Group.self) {
                Field("id", at: \.id)
                Field("name", at: \.name)
            }

            Type(Property.self) {
                Field("id", at: \.id)
                Field("name", at: \.name)
                Field("value", at: \.value)
            }

            Type(Topic.self) {
                Field("id", at: \.id)
                Field("parentId", at: \.parentID)
                Field("title", at: \.title)
                Field("imageURL", at: \.imageURL)
                Field("groups", at: \.groups )
                Field("intensity", at: \.intensity)
                Field("weight", at: \.weight)
            }

            Type(Keyword.self) {
                Field("id", at: \.id)
                Field("name", at: \.name)
                Field("occurrence", at: \.occurrence)
            }

            Type(Feed.self) {
                Field("id", at: \.id)
                Field("type", at: \.type)
                Field("url", at: \.url)
                Field("displayName", at: \.displayName)
                Field("topics", at: \.topics)
                Field("channels", at: \.channels)
            }

            Type(FeedItem.self) {
                Field("id", at: \.id)
                Field("name", at: \.name)
                Field("imageURL", at: \.imageURL)
                Field("summary", at: \.summary)
                Field("published", at: \.published)
                Field("readTime", at: \.readTime)
                Field("url", at: \.url)
                Field("topics", at: \.topics)
                Field("keywords", at: \.keywords)
                Field("properties", at: \.properties)
                Field("locale", at: \.locale)
                Field("feed", at: \.feed)
                Field("source", at: \.source)
                Field("displaySource", at: \.displaySource)
            }

            ConnectionType(FeedItem.self)

            Query {
                Field("topics", at: GraphQLResolver.topics) {
                    Argument("groups", at: \.groups)
                        .defaultValue([])
                }
                Field("feeds", at: GraphQLResolver.feeds) {
                    Argument("channels", at: \.channels)
                        .defaultValue([])
                }
                Field("channels", at: GraphQLResolver.channels)
                Field("properties", at: GraphQLResolver.properties)
                Field("groups", at: GraphQLResolver.groups)

                Field("feedItems", at: GraphQLResolver.feedItems) {
                    Argument("first", at: \.first)
                    Argument("after", at: \.after)
                    Argument("notIn", at: \.notIn)
                        .defaultValue([])
                    Argument("topics", at: \.topics)
                        .defaultValue([])
                    Argument("topicsNotIn", at: \.topicsNotIn)
                        .defaultValue([])
                    Argument("feedURLIn", at: \.feedURLIn)
                        .defaultValue([])
                    Argument("feedURLNotIn", at: \.feedURLNotIn)
                        .defaultValue([])
                    Argument("channels", at: \.channels)
                        .defaultValue([])
                    Argument("feeds", at: \.feeds)
                        .defaultValue([])
                    Argument("beforeDate", at: \.beforeDate)
                    Argument("afterDate", at: \.afterDate)
                    Argument("locales", at: \.locales)
                        .defaultValue([])
                }
            }
        }
    }
}

Here's an example of the resolver for the schema above:

import Entities
import NIO
import Graphiti
import Foundation

public struct GraphQLResolver {
    public init() {}
}

// MARK: Topic Queries
extension GraphQLResolver {
    struct TopicsArguments: Decodable {
        let groups: [String]
    }

    func topics(context: GraphQLContext, arguments: TopicsArguments) -> EventLoopFuture<[Topic]> {
        context.app.getTopics(groups: arguments.groups)
    }
}

// MARK: Group Queries
extension GraphQLResolver {
    func groups(context: GraphQLContext, arguments: NoArguments) -> EventLoopFuture<[Group]> {
        context.app.getGroups()
    }
}

// MARK: Channel Queries
extension GraphQLResolver {
    func channels(context: GraphQLContext, arguments: NoArguments) -> EventLoopFuture<[Entities.Channel]> {
        context.app.getChannels()
    }
}

// MARK: Property Queries
extension GraphQLResolver {
    func properties(context: GraphQLContext, arguments: NoArguments) -> EventLoopFuture<[Property]> {
        context.app.getProperties()
    }
}

// MARK: Feed Queries
extension GraphQLResolver {
    struct FeedsArguments: Decodable {
        let channels: [String]
    }

    func feeds(context: GraphQLContext, arguments: FeedsArguments) -> EventLoopFuture<[Feed]> {
        context.app.getFeeds(channels: arguments.channels, accessToken: context.accessToken)
    }
}

// MARK: Feed Item Queries
extension GraphQLResolver {
    struct FeedItemsArguments: ForwardPaginatable {
        let first: Int?
        let after: String?
        let notIn: [Int]
        let topics: [String]
        let topicsNotIn: [String]
        let feedURLIn: [String]
        let feedURLNotIn: [String]
        let channels: [String]
        let feeds: [Int]
        let beforeDate: Date?
        let afterDate: Date?
        let locales: [String]
    }

    func feedItems(context: GraphQLContext, arguments: FeedItemsArguments) -> EventLoopFuture<Connection<FeedItem>> {
       context.app
            .getFeedItems(
                first: arguments.first,
                after: arguments.after.flatMap({ FeedItem.Cursor(base64EncodedString: $0) }),
                notIn: arguments.notIn,
                topics: arguments.topics,
                topicsNotIn: arguments.topicsNotIn,
                feedURLIn: arguments.feedURLIn,
                feedURLNotIn: arguments.feedURLNotIn,
                channels: arguments.channels,
                feeds: arguments.feeds,
                beforeDate: arguments.beforeDate,
                afterDate: arguments.afterDate,
                locales: arguments.locales
            )
            .connection(from: arguments, makeCursor: FeedItem.makeCursor)
    }
}

In the resolver above we pass a makeCursor function that creates a cursor for FeedItem. If the type you want to create a connection for is type that conforms to Identifiable, you don't need to provide this function. However, you might want to create a custom cursor to add any data you want.

MaxDesiatov commented 3 years ago

Awesome, thanks!

paulofaria commented 3 years ago

@cshadek your issue is harder to solve. We would need to do a double pass when reading the schema. It is possible. However, I don't have much time, these days, to implement that. Feel free to send a PR working on this and I can help with pointers and such.

cshadek commented 1 year ago

I believe this is no longer an issue after #84. Maybe this should be closed? We could add an example of connections to the Usage Guide.

cshadek commented 1 year ago

Closing because #108 adds documentation to the Usage Guide to show how to use connections. Furthermore, other recent improvements that were introduced alongside PartialSchema should solve the double pass issue.