Skip to content

Commit

Permalink
Add support for connections.
Browse files Browse the repository at this point in the history
  • Loading branch information
paulofaria committed Jun 30, 2020
1 parent 2622195 commit eeee7d3
Show file tree
Hide file tree
Showing 25 changed files with 380 additions and 87 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let package = Package(
.library(name: "Graphiti", targets: ["Graphiti"]),
],
dependencies: [
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", .upToNextMajor(from: "1.1.2")),
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", .upToNextMajor(from: "1.1.3")),
.package(url: "https://github.com/wickwirew/Runtime.git", .upToNextMinor(from: "2.1.0")),
],
targets: [
Expand Down
9 changes: 9 additions & 0 deletions Sources/Graphiti/BackwardPaginationArguments.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public protocol BackwardPaginatable : Decodable {
var last: Int? { get }
var before: String? { get }
}

public struct BackwardPaginationArguments : BackwardPaginatable {
public let last: Int?
public let before: String?
}
5 changes: 5 additions & 0 deletions Sources/Graphiti/Component.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
public class Component<RootType : Keyable, Context> {
let name: String
var description: String? = nil

init(name: String) {
self.name = name
}

public func description(_ description: String) -> Self {
self.description = description
return self
Expand Down
40 changes: 37 additions & 3 deletions Sources/Graphiti/ComponentsInitializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,40 @@ public final class ComponentsInitializer<RootType : Keyable, Context> {
return ComponentInitializer(component)
}

// MARK: Extensions

public func connection<ObjectType : Encodable & Keyable>(
_ type: ObjectType.Type,
name: String? = nil
) {
if !components.contains(where: { $0.name == "PageInfo" }) {
self.type(PageInfo.self) { type in
type.field(.hasPreviousPage, at: \.hasPreviousPage)
type.field(.hasNextPage, at: \.hasNextPage)
type.field(.startCursor, at: \.startCursor)
type.field(.endCursor, at: \.endCursor)
}
}

self.type(Edge<ObjectType>.self) { type in
type.field(.node, at: \.node)
type.field(.cursor, at: \.cursor)
}

self.type(Connection<ObjectType>.self) { type in
type.field(.edges, at: \.edges)
type.field(.pageInfo, at: \.pageInfo)
}
}

@discardableResult
public func dateScalar(
formatter: DateFormatter
formatter: DateFormatter,
name: String? = nil
) -> ComponentInitializer<RootType, Context> {
scalar(
Date.self,
name: name,
serialize: { date in
.string(formatter.string(from: date))
},
Expand All @@ -196,9 +224,12 @@ public final class ComponentsInitializer<RootType : Keyable, Context> {
}

@discardableResult
public func urlScalar() -> ComponentInitializer<RootType, Context> {
public func urlScalar(
name: String? = nil
) -> ComponentInitializer<RootType, Context> {
scalar(
URL.self,
name: name,
serialize: { url in
.string(url.absoluteString)
},
Expand All @@ -217,9 +248,12 @@ public final class ComponentsInitializer<RootType : Keyable, Context> {
}

@discardableResult
public func uuidScalar() -> ComponentInitializer<RootType, Context> {
public func uuidScalar(
name: String? = nil
) -> ComponentInitializer<RootType, Context> {
scalar(
UUID.self,
name: name,
serialize: { uuid in
.string(uuid.uuidString)
},
Expand Down
163 changes: 163 additions & 0 deletions Sources/Graphiti/Connection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import Foundation
import NIO
import GraphQL

public struct Connection<T : Encodable> : Encodable, Keyable {
public enum Keys : String {
case edges
case pageInfo
}

let edges: [Edge<T>]
let pageInfo: PageInfo
}

@available(OSX 10.15, *)
public extension EventLoopFuture where Value : Sequence, Value.Element : Codable & Identifiable {
func connection(from arguments: Paginatable) -> EventLoopFuture<Connection<Value.Element>> {
flatMapThrowing { value in
try value.connection(from: arguments)
}
}

func connection(from arguments: ForwardPaginatable) -> EventLoopFuture<Connection<Value.Element>> {
flatMapThrowing { value in
try value.connection(from: arguments)
}
}

func connection(from arguments: BackwardPaginatable) -> EventLoopFuture<Connection<Value.Element>> {
flatMapThrowing { value in
try value.connection(from: arguments)
}
}
}

@available(OSX 10.15, *)
extension Sequence where Element : Codable & Identifiable {
func connection(from arguments: Paginatable) throws -> Connection<Element> {
try connect(to: Array(self), arguments: PaginationArguments(arguments))
}

func connection(from arguments: ForwardPaginatable) throws -> Connection<Element> {
try connect(to: Array(self), arguments: PaginationArguments(arguments))
}

func connection(from arguments: BackwardPaginatable) throws -> Connection<Element> {
try connect(to: Array(self), arguments: PaginationArguments(arguments))
}
}

@available(OSX 10.15, *)
func connect<T : Codable & Identifiable>(
to elements: [T],
arguments: PaginationArguments
) throws -> Connection<T> {
let edges = elements.map { element in
Edge<T>(node: element, cursor: "\(element.id)".base64Encoded()!)
}

let cursorEdges = slicingCursor(edges: edges, arguments: arguments)
let countEdges = try slicingCount(edges: cursorEdges, arguments: arguments)

return Connection(
edges: countEdges,
pageInfo: PageInfo(
hasPreviousPage: hasPreviousPage(edges: cursorEdges, arguments: arguments),
hasNextPage: hasNextPage(edges: cursorEdges, arguments: arguments),
startCursor: countEdges.first.map(\.cursor),
endCursor: countEdges.last.map(\.cursor)
)
)
}

func slicingCursor<T : Codable>(
edges: [Edge<T>],
arguments: PaginationArguments
) -> ArraySlice<Edge<T>> {
var edges = ArraySlice(edges)

if
let after = arguments.after,
let afterIndex = edges
.firstIndex(where: { $0.cursor == after })?
.advanced(by: 1)
{
edges = edges[afterIndex...]
}

if
let before = arguments.before,
let beforeIndex = edges
.firstIndex(where: { $0.cursor == before })
{
edges = edges[..<beforeIndex]
}

return edges
}

func slicingCount<T : Codable>(
edges: ArraySlice<Edge<T>>,
arguments: PaginationArguments
) throws -> Array<Edge<T>> {
var edges = edges

if let first = arguments.first {
if first < 0 {
throw GraphQLError(
message: #"Invalid agurment "first". Argument must be a positive integer."#
)
}

edges = edges.prefix(first)
}

if let last = arguments.last {
if last < 0 {
throw GraphQLError(
message: #"Invalid agurment "last". Argument must be a positive integer."#
)
}

edges = edges.suffix(last)
}

return Array(edges)
}

func hasPreviousPage<T : Codable>(
edges: ArraySlice<Edge<T>>,
arguments: PaginationArguments
) -> Bool {
if let last = arguments.last {
return edges.count > last
}

return false
}

func hasNextPage<T : Codable>(
edges: ArraySlice<Edge<T>>,
arguments: PaginationArguments
) -> Bool {
if let first = arguments.first {
return edges.count > first
}

return false
}

extension String {
func base64Encoded() -> String? {
return data(using: .utf8)?.base64EncodedString()
}

func base64Decoded() -> String? {
guard let data = Data(base64Encoded: self) else {
return nil
}

return String(data: data, encoding: .utf8)
}
}
15 changes: 15 additions & 0 deletions Sources/Graphiti/Edge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
protocol Edgeable {
associatedtype T : Encodable
var node: T { get }
var cursor: String { get }
}

struct Edge<T : Encodable> : Edgeable, Encodable, Keyable {
enum Keys : String {
case node
case cursor
}

let node: T
let cursor: String
}
41 changes: 0 additions & 41 deletions Sources/Graphiti/Encodable.swift

This file was deleted.

5 changes: 2 additions & 3 deletions Sources/Graphiti/Enum.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import GraphQL

public final class Enum<RootType : Keyable, Context, EnumType : Enumerable> : Component<RootType, Context> {
private let name: String?
private let values: [Value<EnumType>]

override func update(builder: SchemaBuilder) throws {
let enumType = try GraphQLEnumType(
name: name ?? Reflection.name(for: EnumType.self),
name: name,
description: description,
values: values.reduce(into: [:]) { result, value in
result[value.value.rawValue] = GraphQLEnumValue(
Expand All @@ -25,7 +24,7 @@ public final class Enum<RootType : Keyable, Context, EnumType : Enumerable> : Co
name: String?,
values: [Value<EnumType>]
) {
self.name = name
self.values = values
super.init(name: name ?? Reflection.name(for: EnumType.self))
}
}
8 changes: 4 additions & 4 deletions Sources/Graphiti/FieldInitializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ public final class FieldInitializer<ObjectType, Keys : RawRepresentable, Context

@discardableResult
public func argument<Argument>(
_ name: Keys,
_ name: Arguments.Keys,
at keyPath: KeyPath<Arguments, Argument>,
description: String
) -> Self {
) -> Self where Arguments : Keyable {
field.argumentsDescriptions[name.rawValue] = description
return self
}

@discardableResult
public func argument<Argument : Encodable>(
_ name: Keys,
_ name: Arguments.Keys,
at keyPath: KeyPath<Arguments, Argument>,
defaultValue: Argument
) -> Self {
) -> Self where Arguments : Keyable {
field.argumentsDefaultValues[name.rawValue] = AnyEncodable(defaultValue)
return self
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/Graphiti/ForwardPaginationArguments.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public protocol ForwardPaginatable : Decodable {
var first: Int? { get }
var after: String? { get }
}

public struct ForwardPaginationArguments : ForwardPaginatable {
public let first: Int?
public let after: String?
}
Loading

0 comments on commit eeee7d3

Please sign in to comment.