Graphiti icon indicating copy to clipboard operation
Graphiti copied to clipboard

Is there an example of a Graphiti schema using connections?

Open cshadek opened this issue 5 years ago • 7 comments

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!

cshadek avatar Jul 26 '20 02:07 cshadek

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

ConnectionType(User.self),

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

paulofaria avatar Jul 29 '20 23:07 paulofaria

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.

cshadek avatar Jul 30 '20 15:07 cshadek

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?

MaxDesiatov avatar Feb 04 '21 19:02 MaxDesiatov

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

paulofaria avatar Feb 05 '21 20:02 paulofaria

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.

paulofaria avatar Feb 10 '21 17:02 paulofaria

Awesome, thanks!

MaxDesiatov avatar Feb 10 '21 17:02 MaxDesiatov

@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.

paulofaria avatar Feb 10 '21 17:02 paulofaria

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 avatar Jan 31 '23 17:01 cshadek

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.

cshadek avatar Mar 13 '23 05:03 cshadek