Skip to content

Commit eeee7d3

Browse files
committed
Add support for connections.
1 parent 2622195 commit eeee7d3

25 files changed

+380
-87
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ let package = Package(
77
.library(name: "Graphiti", targets: ["Graphiti"]),
88
],
99
dependencies: [
10-
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", .upToNextMajor(from: "1.1.2")),
10+
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", .upToNextMajor(from: "1.1.3")),
1111
.package(url: "https://github.com/wickwirew/Runtime.git", .upToNextMinor(from: "2.1.0")),
1212
],
1313
targets: [
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
public protocol BackwardPaginatable : Decodable {
2+
var last: Int? { get }
3+
var before: String? { get }
4+
}
5+
6+
public struct BackwardPaginationArguments : BackwardPaginatable {
7+
public let last: Int?
8+
public let before: String?
9+
}

Sources/Graphiti/Component.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
public class Component<RootType : Keyable, Context> {
2+
let name: String
23
var description: String? = nil
34

5+
init(name: String) {
6+
self.name = name
7+
}
8+
49
public func description(_ description: String) -> Self {
510
self.description = description
611
return self

Sources/Graphiti/ComponentsInitializer.swift

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,40 @@ public final class ComponentsInitializer<RootType : Keyable, Context> {
172172
return ComponentInitializer(component)
173173
}
174174

175+
// MARK: Extensions
176+
177+
public func connection<ObjectType : Encodable & Keyable>(
178+
_ type: ObjectType.Type,
179+
name: String? = nil
180+
) {
181+
if !components.contains(where: { $0.name == "PageInfo" }) {
182+
self.type(PageInfo.self) { type in
183+
type.field(.hasPreviousPage, at: \.hasPreviousPage)
184+
type.field(.hasNextPage, at: \.hasNextPage)
185+
type.field(.startCursor, at: \.startCursor)
186+
type.field(.endCursor, at: \.endCursor)
187+
}
188+
}
189+
190+
self.type(Edge<ObjectType>.self) { type in
191+
type.field(.node, at: \.node)
192+
type.field(.cursor, at: \.cursor)
193+
}
194+
195+
self.type(Connection<ObjectType>.self) { type in
196+
type.field(.edges, at: \.edges)
197+
type.field(.pageInfo, at: \.pageInfo)
198+
}
199+
}
200+
175201
@discardableResult
176202
public func dateScalar(
177-
formatter: DateFormatter
203+
formatter: DateFormatter,
204+
name: String? = nil
178205
) -> ComponentInitializer<RootType, Context> {
179206
scalar(
180207
Date.self,
208+
name: name,
181209
serialize: { date in
182210
.string(formatter.string(from: date))
183211
},
@@ -196,9 +224,12 @@ public final class ComponentsInitializer<RootType : Keyable, Context> {
196224
}
197225

198226
@discardableResult
199-
public func urlScalar() -> ComponentInitializer<RootType, Context> {
227+
public func urlScalar(
228+
name: String? = nil
229+
) -> ComponentInitializer<RootType, Context> {
200230
scalar(
201231
URL.self,
232+
name: name,
202233
serialize: { url in
203234
.string(url.absoluteString)
204235
},
@@ -217,9 +248,12 @@ public final class ComponentsInitializer<RootType : Keyable, Context> {
217248
}
218249

219250
@discardableResult
220-
public func uuidScalar() -> ComponentInitializer<RootType, Context> {
251+
public func uuidScalar(
252+
name: String? = nil
253+
) -> ComponentInitializer<RootType, Context> {
221254
scalar(
222255
UUID.self,
256+
name: name,
223257
serialize: { uuid in
224258
.string(uuid.uuidString)
225259
},

Sources/Graphiti/Connection.swift

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import Foundation
2+
import NIO
3+
import GraphQL
4+
5+
public struct Connection<T : Encodable> : Encodable, Keyable {
6+
public enum Keys : String {
7+
case edges
8+
case pageInfo
9+
}
10+
11+
let edges: [Edge<T>]
12+
let pageInfo: PageInfo
13+
}
14+
15+
@available(OSX 10.15, *)
16+
public extension EventLoopFuture where Value : Sequence, Value.Element : Codable & Identifiable {
17+
func connection(from arguments: Paginatable) -> EventLoopFuture<Connection<Value.Element>> {
18+
flatMapThrowing { value in
19+
try value.connection(from: arguments)
20+
}
21+
}
22+
23+
func connection(from arguments: ForwardPaginatable) -> EventLoopFuture<Connection<Value.Element>> {
24+
flatMapThrowing { value in
25+
try value.connection(from: arguments)
26+
}
27+
}
28+
29+
func connection(from arguments: BackwardPaginatable) -> EventLoopFuture<Connection<Value.Element>> {
30+
flatMapThrowing { value in
31+
try value.connection(from: arguments)
32+
}
33+
}
34+
}
35+
36+
@available(OSX 10.15, *)
37+
extension Sequence where Element : Codable & Identifiable {
38+
func connection(from arguments: Paginatable) throws -> Connection<Element> {
39+
try connect(to: Array(self), arguments: PaginationArguments(arguments))
40+
}
41+
42+
func connection(from arguments: ForwardPaginatable) throws -> Connection<Element> {
43+
try connect(to: Array(self), arguments: PaginationArguments(arguments))
44+
}
45+
46+
func connection(from arguments: BackwardPaginatable) throws -> Connection<Element> {
47+
try connect(to: Array(self), arguments: PaginationArguments(arguments))
48+
}
49+
}
50+
51+
@available(OSX 10.15, *)
52+
func connect<T : Codable & Identifiable>(
53+
to elements: [T],
54+
arguments: PaginationArguments
55+
) throws -> Connection<T> {
56+
let edges = elements.map { element in
57+
Edge<T>(node: element, cursor: "\(element.id)".base64Encoded()!)
58+
}
59+
60+
let cursorEdges = slicingCursor(edges: edges, arguments: arguments)
61+
let countEdges = try slicingCount(edges: cursorEdges, arguments: arguments)
62+
63+
return Connection(
64+
edges: countEdges,
65+
pageInfo: PageInfo(
66+
hasPreviousPage: hasPreviousPage(edges: cursorEdges, arguments: arguments),
67+
hasNextPage: hasNextPage(edges: cursorEdges, arguments: arguments),
68+
startCursor: countEdges.first.map(\.cursor),
69+
endCursor: countEdges.last.map(\.cursor)
70+
)
71+
)
72+
}
73+
74+
func slicingCursor<T : Codable>(
75+
edges: [Edge<T>],
76+
arguments: PaginationArguments
77+
) -> ArraySlice<Edge<T>> {
78+
var edges = ArraySlice(edges)
79+
80+
if
81+
let after = arguments.after,
82+
let afterIndex = edges
83+
.firstIndex(where: { $0.cursor == after })?
84+
.advanced(by: 1)
85+
{
86+
edges = edges[afterIndex...]
87+
}
88+
89+
if
90+
let before = arguments.before,
91+
let beforeIndex = edges
92+
.firstIndex(where: { $0.cursor == before })
93+
{
94+
edges = edges[..<beforeIndex]
95+
}
96+
97+
return edges
98+
}
99+
100+
func slicingCount<T : Codable>(
101+
edges: ArraySlice<Edge<T>>,
102+
arguments: PaginationArguments
103+
) throws -> Array<Edge<T>> {
104+
var edges = edges
105+
106+
if let first = arguments.first {
107+
if first < 0 {
108+
throw GraphQLError(
109+
message: #"Invalid agurment "first". Argument must be a positive integer."#
110+
)
111+
}
112+
113+
edges = edges.prefix(first)
114+
}
115+
116+
if let last = arguments.last {
117+
if last < 0 {
118+
throw GraphQLError(
119+
message: #"Invalid agurment "last". Argument must be a positive integer."#
120+
)
121+
}
122+
123+
edges = edges.suffix(last)
124+
}
125+
126+
return Array(edges)
127+
}
128+
129+
func hasPreviousPage<T : Codable>(
130+
edges: ArraySlice<Edge<T>>,
131+
arguments: PaginationArguments
132+
) -> Bool {
133+
if let last = arguments.last {
134+
return edges.count > last
135+
}
136+
137+
return false
138+
}
139+
140+
func hasNextPage<T : Codable>(
141+
edges: ArraySlice<Edge<T>>,
142+
arguments: PaginationArguments
143+
) -> Bool {
144+
if let first = arguments.first {
145+
return edges.count > first
146+
}
147+
148+
return false
149+
}
150+
151+
extension String {
152+
func base64Encoded() -> String? {
153+
return data(using: .utf8)?.base64EncodedString()
154+
}
155+
156+
func base64Decoded() -> String? {
157+
guard let data = Data(base64Encoded: self) else {
158+
return nil
159+
}
160+
161+
return String(data: data, encoding: .utf8)
162+
}
163+
}

Sources/Graphiti/Edge.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
protocol Edgeable {
2+
associatedtype T : Encodable
3+
var node: T { get }
4+
var cursor: String { get }
5+
}
6+
7+
struct Edge<T : Encodable> : Edgeable, Encodable, Keyable {
8+
enum Keys : String {
9+
case node
10+
case cursor
11+
}
12+
13+
let node: T
14+
let cursor: String
15+
}

Sources/Graphiti/Encodable.swift

Lines changed: 0 additions & 41 deletions
This file was deleted.

Sources/Graphiti/Enum.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import GraphQL
22

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

76
override func update(builder: SchemaBuilder) throws {
87
let enumType = try GraphQLEnumType(
9-
name: name ?? Reflection.name(for: EnumType.self),
8+
name: name,
109
description: description,
1110
values: values.reduce(into: [:]) { result, value in
1211
result[value.value.rawValue] = GraphQLEnumValue(
@@ -25,7 +24,7 @@ public final class Enum<RootType : Keyable, Context, EnumType : Enumerable> : Co
2524
name: String?,
2625
values: [Value<EnumType>]
2726
) {
28-
self.name = name
2927
self.values = values
28+
super.init(name: name ?? Reflection.name(for: EnumType.self))
3029
}
3130
}

Sources/Graphiti/FieldInitializer.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,20 @@ public final class FieldInitializer<ObjectType, Keys : RawRepresentable, Context
2121

2222
@discardableResult
2323
public func argument<Argument>(
24-
_ name: Keys,
24+
_ name: Arguments.Keys,
2525
at keyPath: KeyPath<Arguments, Argument>,
2626
description: String
27-
) -> Self {
27+
) -> Self where Arguments : Keyable {
2828
field.argumentsDescriptions[name.rawValue] = description
2929
return self
3030
}
3131

3232
@discardableResult
3333
public func argument<Argument : Encodable>(
34-
_ name: Keys,
34+
_ name: Arguments.Keys,
3535
at keyPath: KeyPath<Arguments, Argument>,
3636
defaultValue: Argument
37-
) -> Self {
37+
) -> Self where Arguments : Keyable {
3838
field.argumentsDefaultValues[name.rawValue] = AnyEncodable(defaultValue)
3939
return self
4040
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
public protocol ForwardPaginatable : Decodable {
2+
var first: Int? { get }
3+
var after: String? { get }
4+
}
5+
6+
public struct ForwardPaginationArguments : ForwardPaginatable {
7+
public let first: Int?
8+
public let after: String?
9+
}

0 commit comments

Comments
 (0)