├── .github
├── FUNDING.yml
└── workflows
│ ├── documentation.yml
│ └── swift.yml
├── .gitignore
├── .jazzy.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── CContext
│ ├── CContext.c
│ └── include
│ │ ├── CContext.h
│ │ └── CContext.h.gyb
├── GraphZahl
│ ├── Resolution
│ │ ├── InputResolvable
│ │ │ ├── Implementations
│ │ │ │ ├── Array+InputResolvable.swift
│ │ │ │ ├── GraphQLInputObject.swift
│ │ │ │ ├── JSONOptional.swift
│ │ │ │ ├── Optional+InputResolvable.swift
│ │ │ │ └── RawRepresentable+InputResolvable.swift
│ │ │ └── InputResolvable.swift
│ │ ├── OutputResolvable
│ │ │ ├── DelegatedOutputResolvable.swift
│ │ │ ├── Implementations
│ │ │ │ ├── Array+OutputResolvable.swift
│ │ │ │ ├── Connection
│ │ │ │ │ ├── ConnectionProtocol.swift
│ │ │ │ │ ├── ConnectionWrapper.swift
│ │ │ │ │ ├── ContextBasedConnection.swift
│ │ │ │ │ ├── ContextBasedConnectionWrapper.swift
│ │ │ │ │ ├── EdgeProtocol.swift
│ │ │ │ │ ├── File.swift
│ │ │ │ │ ├── FixedPageSizeIndexedConnection.swift
│ │ │ │ │ ├── IndexConnectionWrapper.swift
│ │ │ │ │ ├── IndexCursorTranslator.swift
│ │ │ │ │ ├── IndexedConnection.swift
│ │ │ │ │ ├── PageInfo.swift
│ │ │ │ │ └── StandardEdge.swift
│ │ │ │ ├── Future+OutputResolvable.swift
│ │ │ │ ├── Object
│ │ │ │ │ ├── GraphQLObject+OutputResolvable.swift
│ │ │ │ │ ├── GraphQLObject+resolve.swift
│ │ │ │ │ ├── GraphQLObject.swift
│ │ │ │ │ └── PropertyWrappers
│ │ │ │ │ │ ├── CustomGraphQLProperty.swift
│ │ │ │ │ │ ├── Ignore.swift
│ │ │ │ │ │ ├── Inline.swift
│ │ │ │ │ │ ├── InlineAsInterface.swift
│ │ │ │ │ │ ├── LazyInline.swift
│ │ │ │ │ │ ├── LazyInlineAsInterface.swift
│ │ │ │ │ │ └── PropertyResult.swift
│ │ │ │ └── Optional+OutputResolvable.swift
│ │ │ ├── Node.swift
│ │ │ ├── Output.swift
│ │ │ └── OutputResolvable.swift
│ │ ├── Resolution.swift
│ │ ├── Resolvable
│ │ │ ├── ConcreteResolvable.swift
│ │ │ ├── Implementations
│ │ │ │ ├── Array+Resolvable.swift
│ │ │ │ ├── Future+Resolvable.swift
│ │ │ │ └── Optional+Resolvable.swift
│ │ │ ├── KeyPathListable.swift
│ │ │ ├── Resolvable.swift
│ │ │ └── ValueResolvable.swift
│ │ ├── Root
│ │ │ ├── EmptyRootType.swift
│ │ │ ├── GraphQLRootType.swift
│ │ │ ├── GraphQLSchema+perform.swift
│ │ │ ├── GraphQLSchema+resolve.swift
│ │ │ ├── GraphQLSchema.swift
│ │ │ ├── MandatoryRootType.swift
│ │ │ ├── MutationType.swift
│ │ │ └── QueryType.swift
│ │ ├── Scalar
│ │ │ ├── Enum
│ │ │ │ ├── GraphQLEnum.swift
│ │ │ │ └── KeyPath+GraphQLEnum.swift
│ │ │ ├── Scalar
│ │ │ │ ├── GraphQLScalar+InputResolvable.swift
│ │ │ │ ├── GraphQLScalar+OutputResolvable.swift
│ │ │ │ ├── GraphQLScalar.swift
│ │ │ │ ├── Implementations
│ │ │ │ │ ├── Bool+Scalar.swift
│ │ │ │ │ ├── Double+Scalar.swift
│ │ │ │ │ ├── Float+Scalar.swift
│ │ │ │ │ ├── Int+Scalar.swift
│ │ │ │ │ ├── String+Scalar.swift
│ │ │ │ │ └── UUID+Scalar.swift
│ │ │ │ └── RawRepresentable+GraphQLScalar.swift
│ │ │ ├── ScalarTypeError.swift
│ │ │ ├── Type
│ │ │ │ └── ScalarType.swift
│ │ │ └── Value
│ │ │ │ ├── IDValue.swift
│ │ │ │ ├── ScalarValue+decode.swift
│ │ │ │ ├── ScalarValue+init.swift
│ │ │ │ └── ScalarValue.swift
│ │ └── Union
│ │ │ └── GraphQLUnionType.swift
│ └── Utils
│ │ ├── ContextKey+ViewerContext.swift
│ │ ├── Dictionary+getOrPut.swift
│ │ ├── Lock.swift
│ │ ├── Method Dispatching
│ │ ├── Default Value
│ │ │ ├── Argument+defaultValue.swift
│ │ │ └── Argument+defaultValue.swift.gyb
│ │ ├── MethodInfo+call.swift
│ │ ├── MethodInfo+call.swift.gyb
│ │ ├── Register Usage
│ │ │ ├── RegisterUsage.swift
│ │ │ ├── RegisterUsage.swift.gyb
│ │ │ ├── Resolvable
│ │ │ │ ├── MethodCallResolvable.swift
│ │ │ │ ├── MethodCallResolvable.swift.gyb
│ │ │ │ └── SpecialCases.swift
│ │ │ └── Resolve
│ │ │ │ ├── FunctionResultDecoder.swift
│ │ │ │ └── resolveArguments.swift
│ │ ├── typeInfo.swift
│ │ └── typeInfo.swift.gyb
│ │ ├── MethodInfo+resolve.swift
│ │ ├── PropertyInfo+resolve.swift
│ │ ├── String+camelCasing.swift
│ │ └── String+deletingPrefix.swift
└── GraphZahlTests
│ ├── CallingTests.swift
│ └── SchemaResolutionTests.swift
├── demo
├── helloworld.png
├── nestedobject.png
├── object.png
└── urlscalar.png
├── logo.png
└── useGYB
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [nerdsupremacist]
2 |
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: Documentation
2 |
3 | on:
4 | push:
5 | branches: [ develop ]
6 |
7 | jobs:
8 | deploy_docs:
9 | runs-on: macos-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Publish Jazzy Docs
13 | uses: steven0351/publish-jazzy-docs@v1.1.1
14 | with:
15 | personal_access_token: ${{ secrets.ACCESS_TOKEN }}
16 | config: .jazzy.yml
17 |
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on:
4 | push:
5 | branches: [ develop ]
6 | pull_request:
7 | branches: [ develop ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: macos-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Build
17 | run: swift build -v
18 | - name: Run tests
19 | run: swift test -v
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.jazzy.yml:
--------------------------------------------------------------------------------
1 | module: GraphZahl
2 | author: Mathias Quintero
3 | theme: fullwidth
4 | output: ./docs
5 | documentation: ./*.md
6 | author_url: https://github.com/nerdsupremacist
7 | github_url: https://github.com/nerdsupremacist/GraphZahl
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Mathias Quintero
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "ContextKit",
6 | "repositoryURL": "https://github.com/nerdsupremacist/ContextKit.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "d68c64dd4ed4c616bb5fd58f9d19d939a1735f20",
10 | "version": "0.2.1"
11 | }
12 | },
13 | {
14 | "package": "CRuntime",
15 | "repositoryURL": "https://github.com/nerdsupremacist/CRuntime.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "95f911318d8c885f6fc05e971471f94adfd39405",
19 | "version": "2.1.2"
20 | }
21 | },
22 | {
23 | "package": "CwlDemangle",
24 | "repositoryURL": "https://github.com/nerdsupremacist/CwlDemangle.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "ca1ffdc206f76dc5f217f27ea2a0bbd2241a4a92",
28 | "version": "0.1.1-beta.1"
29 | }
30 | },
31 | {
32 | "package": "GraphQL",
33 | "repositoryURL": "https://github.com/nerdsupremacist/GraphQL.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "253b6efa4fdac99a0feef631023232729864a7c1",
37 | "version": "0.12.1-beta.6"
38 | }
39 | },
40 | {
41 | "package": "Runtime",
42 | "repositoryURL": "https://github.com/nerdsupremacist/Runtime.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "32cc77be43c34d95d3fe3d21035b21472e03542a",
46 | "version": "2.1.2-beta.13"
47 | }
48 | },
49 | {
50 | "package": "swift-nio",
51 | "repositoryURL": "https://github.com/apple/swift-nio.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "cf6f2b1807219643270616e03de697b96c46384c",
55 | "version": "2.21.0"
56 | }
57 | }
58 | ]
59 | },
60 | "version": 1
61 | }
62 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "GraphZahl",
8 | platforms: [
9 | .macOS(.v10_15)
10 | ],
11 | products: [
12 | .library(name: "GraphZahl",
13 | targets: ["GraphZahl"]),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/nerdsupremacist/GraphQL.git", from: "0.12.1-beta.6"),
17 | .package(url: "https://github.com/nerdsupremacist/Runtime.git", from: "2.1.2-beta.13"),
18 | .package(url: "https://github.com/nerdsupremacist/ContextKit.git", from: "0.2.1"),
19 | ],
20 | targets: [
21 | .target(
22 | name: "GraphZahl",
23 | dependencies: ["GraphQL", "Runtime", "CContext", "ContextKit"]
24 | ),
25 |
26 | .testTarget(
27 | name: "GraphZahlTests",
28 | dependencies: ["GraphZahl"]),
29 |
30 | .target(name: "CContext"),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/Sources/CContext/CContext.c:
--------------------------------------------------------------------------------
1 |
2 | void set_self_pointer(void *pointer) {
3 | #ifdef __aarch64__
4 | __asm__("mov x20, x0");
5 | #endif
6 | #ifdef __x86_64__
7 | __asm__("movq %rdi, %r13");
8 | #endif
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/CContext/include/CContext.h:
--------------------------------------------------------------------------------
1 | // This file was automatically generated and should not be edited.
2 |
3 | #ifndef selfcontext_h
4 | #define selfcontext_h
5 |
6 |
7 | void set_self_pointer(void *pointer);
8 |
9 |
10 | struct int_function_result {
11 | long res0;
12 | };
13 |
14 |
15 | struct float_function_result {
16 | double res0;
17 | };
18 |
19 |
20 | struct int_int_function_result {
21 | long res0;
22 | long res1;
23 | };
24 |
25 |
26 | struct int_float_function_result {
27 | long res0;
28 | double res1;
29 | };
30 |
31 |
32 | struct float_float_function_result {
33 | double res0;
34 | double res1;
35 | };
36 |
37 |
38 | struct int_int_int_function_result {
39 | long res0;
40 | long res1;
41 | long res2;
42 | };
43 |
44 |
45 | struct int_int_float_function_result {
46 | long res0;
47 | long res1;
48 | double res2;
49 | };
50 |
51 |
52 | struct int_float_float_function_result {
53 | long res0;
54 | double res1;
55 | double res2;
56 | };
57 |
58 |
59 | struct float_float_float_function_result {
60 | double res0;
61 | double res1;
62 | double res2;
63 | };
64 |
65 |
66 | struct int_int_int_int_function_result {
67 | long res0;
68 | long res1;
69 | long res2;
70 | long res3;
71 | };
72 |
73 |
74 | struct int_int_int_float_function_result {
75 | long res0;
76 | long res1;
77 | long res2;
78 | double res3;
79 | };
80 |
81 |
82 | struct int_int_float_float_function_result {
83 | long res0;
84 | long res1;
85 | double res2;
86 | double res3;
87 | };
88 |
89 |
90 | struct int_float_float_float_function_result {
91 | long res0;
92 | double res1;
93 | double res2;
94 | double res3;
95 | };
96 |
97 |
98 | struct float_float_float_float_function_result {
99 | double res0;
100 | double res1;
101 | double res2;
102 | double res3;
103 | };
104 |
105 |
106 | struct int_int_int_int_float_function_result {
107 | long res0;
108 | long res1;
109 | long res2;
110 | long res3;
111 | double res4;
112 | };
113 |
114 |
115 | struct int_int_int_float_float_function_result {
116 | long res0;
117 | long res1;
118 | long res2;
119 | double res3;
120 | double res4;
121 | };
122 |
123 |
124 | struct int_int_float_float_float_function_result {
125 | long res0;
126 | long res1;
127 | double res2;
128 | double res3;
129 | double res4;
130 | };
131 |
132 |
133 | struct int_float_float_float_float_function_result {
134 | long res0;
135 | double res1;
136 | double res2;
137 | double res3;
138 | double res4;
139 | };
140 |
141 |
142 | struct int_int_int_int_float_float_function_result {
143 | long res0;
144 | long res1;
145 | long res2;
146 | long res3;
147 | double res4;
148 | double res5;
149 | };
150 |
151 |
152 | struct int_int_int_float_float_float_function_result {
153 | long res0;
154 | long res1;
155 | long res2;
156 | double res3;
157 | double res4;
158 | double res5;
159 | };
160 |
161 |
162 | struct int_int_float_float_float_float_function_result {
163 | long res0;
164 | long res1;
165 | double res2;
166 | double res3;
167 | double res4;
168 | double res5;
169 | };
170 |
171 |
172 | struct int_int_int_int_float_float_float_function_result {
173 | long res0;
174 | long res1;
175 | long res2;
176 | long res3;
177 | double res4;
178 | double res5;
179 | double res6;
180 | };
181 |
182 |
183 | struct int_int_int_float_float_float_float_function_result {
184 | long res0;
185 | long res1;
186 | long res2;
187 | double res3;
188 | double res4;
189 | double res5;
190 | double res6;
191 | };
192 |
193 |
194 | struct int_int_int_int_float_float_float_float_function_result {
195 | long res0;
196 | long res1;
197 | long res2;
198 | long res3;
199 | double res4;
200 | double res5;
201 | double res6;
202 | double res7;
203 | };
204 |
205 |
206 | #endif
207 |
--------------------------------------------------------------------------------
/Sources/CContext/include/CContext.h.gyb:
--------------------------------------------------------------------------------
1 | // This file was automatically generated and should not be edited.
2 |
3 | #ifndef selfcontext_h
4 | #define selfcontext_h
5 |
6 | %{
7 | def _combos(elements, start_idx, length):
8 | # ignore elements before start_idx
9 | for i in range(start_idx, len(elements)):
10 | elem, count = elements[i]
11 | if count == 0:
12 | continue
13 | # base case: only one element needed
14 | if length == 1:
15 | yield (elem,)
16 | else:
17 | # need more than one elem: mutate the list and recurse
18 | elements[i] = (elem, count - 1)
19 | # when we recurse, we ignore elements before this one
20 | # this ensures we find combinations, not permutations
21 | for combo in _combos(elements, i, length - 1):
22 | yield (elem,) + combo
23 | # fix the list
24 | elements[i] = (elem, count)
25 |
26 |
27 | def combos(elements, length):
28 | if length == 0:
29 | return [[]]
30 | elements = list(elements.items())
31 | return _combos(elements, 0, length)
32 |
33 | allowedResults = 8
34 |
35 | allowedTypes = { "int" : "long", "float" : "double" }
36 | allowedResultMap = { "int" : 4, "float" : 4 }
37 | }%
38 |
39 | void set_self_pointer(void *pointer);
40 |
41 | % for numberOfResults in range(1, allowedResults + 1):
42 | % for combination in combos(allowedResultMap, numberOfResults):
43 |
44 | %{ name = "_".join(combination) }%
45 | struct ${name}_function_result {
46 | % for index, item in enumerate(combination):
47 | ${allowedTypes[item]} res${index};
48 | % end
49 | };
50 |
51 | % end
52 | % end
53 |
54 | #endif
55 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/InputResolvable/Implementations/Array+InputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 |
5 | /**
6 | # Conditional Conformance
7 |
8 | All arrays of values that can be translated to a GraphQL Value, can themselves be translated to a GraphQL Value
9 | */
10 | extension Array: ValueResolvable where Element: ValueResolvable {
11 |
12 | public func map() throws -> Map {
13 | return .array(try map { try $0.map() })
14 | }
15 |
16 | }
17 |
18 | /**
19 | # Conditional Conformance
20 |
21 | All arrays of values that are GraphQL Inputs, can be Inputs themselves
22 | */
23 | extension Array: InputResolvable where Element: InputResolvable {
24 |
25 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLInputType {
26 | return GraphQLNonNull(GraphQLList(try context.resolve(type: Element.self)))
27 | }
28 |
29 | public static func create(from map: Map) throws -> Array {
30 | return try map.arrayValue().map { try Element.create(from: $0) }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/InputResolvable/Implementations/GraphQLInputObject.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import NIO
5 | import Runtime
6 |
7 | private var propertiesForType = [Int : [String : PropertyInfo]]()
8 |
9 | /**
10 | # GraphQLInputObject
11 |
12 | A GraphQL Input Object is a Struct that can be used as an input for a method.
13 |
14 | ## Example
15 |
16 | If want to provide an API for sorting search results. We can create a struct for those options.
17 |
18 | ```swift
19 | enum Order: String, CaseIterable, GraphQLEnum {
20 | case ascending
21 | case descending
22 | }
23 |
24 | struct SortOptions: GraphQLInputType {
25 | let field: KeyPath
26 | let order: Order
27 | }
28 |
29 | class Query: QueryType {
30 | func search(term: String, sortOptions: SortOptions?) -> [SearchResult] {
31 | ...
32 | }
33 | }
34 | ```
35 |
36 | ## Note
37 |
38 | Input types can be useful to group arguments. We can also use them to group arguments that should always be provided together. Like in our search example, we don't need the user to specify how to sort. But when the user wants the Results sorted by a field, we need to know if they should be sorted in an ascending or descending order. That's why the Input Object makes sense in this scenario.
39 |
40 | ## Details
41 |
42 | - A GraphQL Input Object is Input Resolvable and can be used in any method that should be available in GraphQL
43 | - An Input Object has to be a struct. If it's not a struct, an error will be thrown.
44 | - All the properties in the struct have to be Input Resolvable as well. Otherwise an error will be thrown.
45 | */
46 | public protocol GraphQLInputObject: InputResolvable, ConcreteResolvable, ValueResolvable, KeyPathListable { }
47 |
48 | extension GraphQLInputObject {
49 |
50 | /**
51 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
52 | */
53 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLInputType {
54 | let kind = try typeInfo(of: Self.self, .kind)
55 | guard kind == .struct else { throw Resolution.Error.inputObjectIsNotAStruct(type: Self.self) }
56 |
57 | let fields = try properties().mapValues { property -> InputObjectField in
58 | guard let type = property.type as? InputResolvable.Type else {
59 | throw Resolution.Error.invalidPropertyInInputObject(name: property.name, type: property.type, ownerType: Self.self)
60 | }
61 | return InputObjectField(type: try context.resolve(type: type))
62 | }
63 |
64 | return GraphQLNonNull(try GraphQLInputObjectType(name: concreteTypeName, fields: fields))
65 | }
66 |
67 | /**
68 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
69 | */
70 | public static func create(from map: Map) throws -> Self {
71 | let dictionary = try map.dictionaryValue()
72 | return try createInstance { property in
73 | guard let type = property.type as? InputResolvable.Type else {
74 | throw Resolution.Error.invalidPropertyInInputObject(name: property.name, type: property.type, ownerType: Self.self)
75 | }
76 | let value = dictionary[property.name.deleting(prefix: "_")]
77 | if let value = value {
78 | return try type.create(from: value)
79 | } else {
80 | return try type.createFromMissingKey()
81 | }
82 | }
83 | }
84 |
85 | /**
86 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
87 | */
88 | public func map() throws -> Map {
89 | let dictionary = try Self.properties().compactMapValues { property -> Map? in
90 | guard let value = try property.get(from: self) as? ValueResolvable else {
91 | throw Resolution.Error.invalidPropertyInInputObject(name: property.name, type: property.type, ownerType: Self.self)
92 | }
93 | return try value.map()
94 | }
95 | return .dictionary(dictionary)
96 | }
97 |
98 | private static func properties() throws -> [String : PropertyInfo] {
99 | return try propertiesForType.getOrPut(unsafeBitCast(Self.self, to: Int.self)) {
100 | let properties = try typeInfo(of: Self.self, .properties)
101 | return Dictionary(properties.map { ($0.name.deleting(prefix: "_"), $0) }) { first, _ in first }
102 | }
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/InputResolvable/Implementations/JSONOptional.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 |
5 | public enum JSONOptional {
6 | case missingKey
7 | case null
8 | case some(Wrapped)
9 | }
10 |
11 | extension JSONOptional {
12 |
13 | public func map(_ transform: (Wrapped) throws -> T) rethrows -> JSONOptional {
14 | switch self {
15 | case .missingKey:
16 | return .missingKey
17 | case .null:
18 | return .null
19 | case .some(let wrapped):
20 | return .some(try transform(wrapped))
21 | }
22 | }
23 |
24 | public func flatMap(_ transform: (Wrapped) throws -> JSONOptional) rethrows -> JSONOptional {
25 | switch self {
26 | case .missingKey:
27 | return .missingKey
28 | case .null:
29 | return .null
30 | case .some(let wrapped):
31 | return try transform(wrapped)
32 | }
33 | }
34 |
35 | }
36 |
37 | extension JSONOptional: Resolvable where Wrapped: Resolvable { }
38 |
39 | extension JSONOptional: InputResolvable where Wrapped: InputResolvable {
40 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLInputType {
41 | return try context.resolve(type: Optional.self)
42 | }
43 |
44 | public static func create(from map: Map) throws -> JSONOptional {
45 | switch map {
46 | case .null:
47 | return .null
48 | default:
49 | return .some(try Wrapped.create(from: map))
50 | }
51 | }
52 |
53 | public static func createFromMissingKey() throws -> JSONOptional {
54 | return .missingKey
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/InputResolvable/Implementations/Optional+InputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 |
5 | /**
6 | # Conditional Conformance
7 |
8 | All optionals of values that can be translated to a GraphQL Value, can themselves be translated to a GraphQL Value
9 | */
10 | extension Optional: ValueResolvable where Wrapped: ValueResolvable {
11 |
12 | public func map() throws -> Map {
13 | return try map { try $0.map() } ?? .null
14 | }
15 |
16 | }
17 |
18 | /**
19 | # Conditional Conformance
20 |
21 | All optionals of values that are GraphQL Inputs, can be Inputs themselves
22 | */
23 | extension Optional: InputResolvable where Wrapped: InputResolvable {
24 |
25 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLInputType {
26 | switch try context.resolve(type: Wrapped.self) {
27 | case let resolved as GraphQLNonNull:
28 | guard let type = resolved.ofType as? GraphQLInputType else { fatalError() }
29 | return type
30 | case let type:
31 | return type
32 | }
33 | }
34 |
35 | public static func create(from map: Map) throws -> Optional {
36 | switch map {
37 | case .null:
38 | return .none
39 | default:
40 | return try Wrapped.create(from: map)
41 | }
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/InputResolvable/Implementations/RawRepresentable+InputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 |
5 | /**
6 | # Conditional Conformance
7 |
8 | All raw representables of values that can be translated to a GraphQL Value, can themselves be translated to a GraphQL Value
9 | */
10 | extension RawRepresentable where Self: ValueResolvable, RawValue: ValueResolvable {
11 |
12 | /**
13 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
14 | */
15 | public func map() throws -> Map {
16 | return try rawValue.map()
17 | }
18 |
19 | }
20 |
21 | /**
22 | # Conditional Conformance
23 |
24 | All raw representables of values that are GraphQL Inputs, can be Inputs themselves
25 | */
26 | extension RawRepresentable where Self: InputResolvable, RawValue: InputResolvable {
27 |
28 | /**
29 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
30 | */
31 | public static func create(from map: Map) throws -> Self {
32 | let rawValue = try RawValue.create(from: map)
33 | guard let value = Self(rawValue: rawValue) else {
34 | throw Resolution.Error.cannotInstantiateTypeFromRawValue(Self.self, rawValue: rawValue)
35 | }
36 | return value
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/InputResolvable/InputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 |
5 | /**
6 | - Attention: this is a low level API. Do not use this directly unless you know what you're doing.
7 |
8 | # InputResolvable
9 |
10 | Input Resolvable describes an Object that can be used as an Argument to a GraphQL Field.
11 |
12 | It means that for a method to be available in GraphQL, all the arguments have to conform to `InputResolvable`
13 |
14 | Input Resolvable Types are:
15 | - Scalars
16 | - Enums
17 | - Input Objects
18 | - Optionals and Arrays through Conditional Conformance
19 | */
20 | public protocol InputResolvable: Resolvable {
21 | /**
22 | - Warning: never call this method directly. Always use: `context.resolve(type: ...)`
23 |
24 | Creates the Definition that will be used from GraphQL
25 |
26 | - Parameter context: Resolution Context where all the type relationships are stored. Use the context to resolve nested types.
27 |
28 | - Returns: A `GraphQLInputType` describing this Type..
29 | */
30 | static func resolve(using context: inout Resolution.Context) throws -> GraphQLInputType
31 |
32 | /**
33 | Instantiates this type from a GraphQL Value. Very similar to decodin with `init(from: Decoder)`
34 |
35 | - Parameter map: Object describing the value .
36 |
37 | - Returns: An instance of the Type
38 |
39 | ## Note
40 |
41 | This function is a static function instead of an initializer.
42 | The reason for that is that this protocol is often implemented in extensions of standard types.
43 | Since one of them is KeyPath, which is a class type.
44 | This can't be an initializer, since required initializers can't be defined in an extension.
45 | */
46 | static func create(from map: Map) throws -> Self
47 |
48 | static func createFromMissingKey() throws -> Self
49 | }
50 |
51 | extension InputResolvable {
52 |
53 | public static func createFromMissingKey() throws -> Self {
54 | return try create(from: .null)
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/DelegatedOutputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import ContextKit
4 | import NIO
5 | import GraphQL
6 |
7 | public protocol DelegatedOutputResolvable: OutputResolvable {
8 | associatedtype Output: OutputResolvable
9 |
10 | func resolve(source: Any,
11 | arguments: [String : Map],
12 | context: MutableContext,
13 | eventLoop: EventLoopGroup) throws -> Output
14 | }
15 |
16 | extension DelegatedOutputResolvable {
17 |
18 | /**
19 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
20 | */
21 | public static var additionalArguments: [String : InputResolvable.Type] {
22 | return Output.additionalArguments
23 | }
24 |
25 | /**
26 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
27 | */
28 | public static func reference(using context: inout Resolution.Context) throws -> GraphQLOutputType {
29 | return try context.reference(for: Output.self)
30 | }
31 |
32 | /**
33 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
34 | */
35 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
36 | return try context.resolve(type: Output.self)
37 | }
38 |
39 | /**
40 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
41 | */
42 | public func resolve(source: Any,
43 | arguments: [String : Map],
44 | context: MutableContext,
45 | eventLoop: EventLoopGroup) throws -> GraphZahl.Output {
46 |
47 | return try resolve(source: source, arguments: arguments, context: context, eventLoop: eventLoop)
48 | .resolve(source: source, arguments: arguments, context: context, eventLoop: eventLoop)
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Array+OutputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import NIO
5 | import ContextKit
6 |
7 | /**
8 | # Conditional Conformance
9 |
10 | All arrays of values that are GraphQL Outputs, can be outputs themselves
11 | */
12 | extension Array: OutputResolvable where Element: OutputResolvable {
13 |
14 | public static var additionalArguments: [String : InputResolvable.Type] {
15 | return Element.additionalArguments
16 | }
17 |
18 | public static func reference(using context: inout Resolution.Context) throws -> GraphQLOutputType {
19 | return GraphQLNonNull(GraphQLList(try context.reference(for: Element.self)))
20 | }
21 |
22 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
23 | return GraphQLNonNull(GraphQLList(try context.resolve(type: Element.self)))
24 | }
25 |
26 | public func resolve(source: Any,
27 | arguments: [String : Map],
28 | context: MutableContext,
29 | eventLoop: EventLoopGroup) throws -> Output {
30 |
31 | return .array(try map { try $0.resolve(source: source, arguments: arguments, context: context, eventLoop: eventLoop) })
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/ConnectionProtocol.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 | import GraphQL
5 | import ContextKit
6 |
7 | /**
8 | # ConnectionProtocol
9 |
10 | Describes an Object used for Cursor-Based Pagination.
11 | This API is modeled after standards set by [Relay](https://relay.dev/docs/en/pagination-container) and [GraphQL](https://graphql.org/learn/pagination/).
12 |
13 | It relies on the usage of the arguments: `first`, `after`, `last` and `before`.
14 | - `first`: Number of items that will be fetched at the start of the list
15 | - `after`: Cursor of the last element in the page that comes before. Signals that all items in the requested page should be strictly after the element with that cursor. The element with the cursor should be excluded
16 | - `last`: Number of items that will be fetched at the end of the list
17 | - `before`: Cursor of the first element in the page that comes after. Signals that all items in the requested page should be strictly before the element with that cursor. The element with the cursor should be excluded
18 |
19 | ## Note
20 |
21 | This is a bare bones implementation of Paging, and is not particularly efficient.
22 |
23 | If you notice that your methods are all sharing a lot of logic together, perhaps you should take a look at `ContextBasedConnection` insttead.
24 | */
25 | public protocol ConnectionProtocol: OutputResolvable, ConcreteResolvable {
26 | /**
27 | Type of Item in the List. Has to conform to `OutputResolvable`
28 | */
29 | associatedtype Node
30 |
31 | /**
32 | Type of Edges in the List.
33 | Edges may contain additional information.
34 |
35 | If no edge type is provided, it will default to the Standard Implementation.
36 | */
37 | associatedtype Edge: EdgeProtocol = StandardEdge where Edge.Node == Node
38 |
39 | /**
40 | Computes the number of items in the list.
41 |
42 | - Returns: A future with the computed count.
43 | */
44 | func totalCount() -> EventLoopFuture
45 |
46 | /**
47 | Computes information about the current page.
48 | This Information includes the start and end cursor and whether or not there's adjacent pages.
49 |
50 | - Parameters:
51 | - first: Number of items that will be fetched at the start of the list
52 | - after: Cursor of the last element in the page that comes before. Signals that all items in the requested page should be strictly after the element with that cursor. The element with the cursor should be excluded
53 | - last: Number of items that will be fetched at the end of the list
54 | - before: Cursor of the first element in the page that comes after. Signals that all items in the requested page should be strictly before the element with that cursor. The element with the cursor should be excluded
55 | - eventLoop: Event Loop Group that can be used to create the futures
56 |
57 | - Returns: A future with the computed Page Info
58 | */
59 | func pageInfo(first: Int?, after: String?, last: Int?, before: String?, eventLoop: EventLoopGroup) -> EventLoopFuture
60 |
61 |
62 | /**
63 | Computes the edges for the current page
64 |
65 | - Parameters:
66 | - first: Number of items that will be fetched at the start of the list
67 | - after: Cursor of the last element in the page that comes before. Signals that all items in the requested page should be strictly after the element with that cursor. The element with the cursor should be excluded
68 | - last: Number of items that will be fetched at the end of the list
69 | - before: Cursor of the first element in the page that comes after. Signals that all items in the requested page should be strictly before the element with that cursor. The element with the cursor should be excluded
70 | - eventLoop: Event Loop Group that can be used to create the futures
71 |
72 | - Returns: A future with an array of edges.
73 | */
74 | func edges(first: Int?, after: String?, last: Int?, before: String?, eventLoop: EventLoopGroup) -> EventLoopFuture<[Edge?]?>
75 | }
76 |
77 | extension ConnectionProtocol {
78 |
79 | public static var concreteTypeName: String {
80 | return "\(Node.concreteTypeName)\(String(describing: Self.self))"
81 | }
82 |
83 | /**
84 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
85 | */
86 | public static var additionalArguments: [String : InputResolvable.Type] {
87 | return ConnectionWrapper.additionalArguments
88 | }
89 |
90 | /**
91 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
92 | */
93 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
94 | return try context.resolve(type: ConnectionWrapper.self)
95 | }
96 |
97 | /**
98 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
99 | */
100 | public func resolve(source: Any,
101 | arguments: [String : Map],
102 | context: MutableContext,
103 | eventLoop: EventLoopGroup) throws -> EventLoopFuture {
104 |
105 | let first = try Optional.create(from: arguments["first"] ?? .null)
106 | let after = try Optional.create(from: arguments["after"] ?? .null)
107 | let last = try Optional.create(from: arguments["last"] ?? .null)
108 | let before = try Optional.create(from: arguments["before"] ?? .null)
109 |
110 | let connection = ConnectionWrapper(connection: self,
111 | eventLoop: eventLoop,
112 | first: first,
113 | after: after,
114 | last: last,
115 | before: before)
116 |
117 | return eventLoop.next().makeSucceededFuture(connection)
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/ConnectionWrapper.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 | import GraphQL
5 | import ContextKit
6 |
7 | class ConnectionWrapper {
8 | private let connection: Connection
9 | private let eventLoop: EventLoopGroup
10 |
11 | private let first: Int?
12 | private let after: String?
13 | private let last: Int?
14 | private let before: String?
15 |
16 | init(connection: Connection,
17 | eventLoop: EventLoopGroup,
18 | first: Int?,
19 | after: String?,
20 | last: Int?,
21 | before: String?) {
22 |
23 | self.connection = connection
24 | self.eventLoop = eventLoop
25 | self.first = first
26 | self.after = after
27 | self.last = last
28 | self.before = before
29 | }
30 | }
31 |
32 | extension ConnectionWrapper {
33 |
34 | private func pageInfo() -> EventLoopFuture {
35 | return connection.pageInfo(first: first, after: after, last: last, before: before, eventLoop: eventLoop)
36 | }
37 |
38 | private func edges() -> EventLoopFuture<[Connection.Edge?]?> {
39 | return connection.edges(first: first, after: after, last: last, before: before, eventLoop: eventLoop)
40 | }
41 |
42 | private func totalCount() -> EventLoopFuture {
43 | return connection.totalCount()
44 | }
45 |
46 | }
47 |
48 | extension ConnectionWrapper: ConcreteResolvable {
49 |
50 | static var concreteTypeName: String {
51 | return Connection.concreteTypeName
52 | }
53 |
54 | }
55 |
56 | extension ConnectionWrapper: OutputResolvable {
57 |
58 | static var additionalArguments: [String : InputResolvable.Type] {
59 | return [
60 | "first" : Int?.self,
61 | "after" : String?.self,
62 | "last" : Int?.self,
63 | "before" : String?.self,
64 | ]
65 | }
66 |
67 | static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
68 | let fields = [
69 | "pageInfo" : GraphQLField(type: try context.reference(for: PageInfo.self)) { (receiver, args, context, eventLoop, _) -> Future in
70 | return try (receiver as! ConnectionWrapper)
71 | .pageInfo()
72 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext, eventLoop: eventLoop)
73 | .convert(eventLoopGroup: eventLoop)
74 | },
75 | "edges" : GraphQLField(type: try context.reference(for: [Connection.Edge?]?.self)) { (receiver, args, context, eventLoop, _) -> Future in
76 | return try (receiver as! ConnectionWrapper)
77 | .edges()
78 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext, eventLoop: eventLoop)
79 | .convert(eventLoopGroup: eventLoop)
80 | },
81 | "totalCount" : GraphQLField(type: try context.reference(for: Int.self)) { (receiver, args, context, eventLoop, _) -> Future in
82 | return try (receiver as! ConnectionWrapper)
83 | .totalCount()
84 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext, eventLoop: eventLoop)
85 | .convert(eventLoopGroup: eventLoop)
86 | },
87 | ]
88 |
89 | return GraphQLNonNull(
90 | try GraphQLObjectType(name: concreteTypeName, fields: fields)
91 | )
92 | }
93 |
94 | func resolve(source: Any, arguments: [String : Map], context: MutableContext, eventLoop: EventLoopGroup) throws -> Output {
95 | return .unsafeReceiver(self)
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/ContextBasedConnection.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 | import GraphQL
5 | import ContextKit
6 |
7 | /**
8 | # ContextBasedConnection
9 |
10 | Describes an Object used for Cursor-Based Pagination.
11 | This API is modeled after standards set by [Relay](https://relay.dev/docs/en/pagination-container) and [GraphQL](https://graphql.org/learn/pagination/).
12 |
13 | It relies on the usage of the arguments: `first`, `after`, `last` and `before`.
14 | - `first`: Number of items that will be fetched at the start of the list
15 | - `after`: Cursor of the last element in the page that comes before. Signals that all items in the requested page should be strictly after the element with that cursor. The element with the cursor should be excluded
16 | - `last`: Number of items that will be fetched at the end of the list
17 | - `before`: Cursor of the first element in the page that comes after. Signals that all items in the requested page should be strictly before the element with that cursor. The element with the cursor should be excluded
18 |
19 | ## Note
20 |
21 | This is very similar to `ConnectionProtocol`.
22 |
23 | `ContextBasedConnection` describes the same functionality of `ConnectionProtocol`but attemps to compute certain context information about a page only once.
24 |
25 | A type implementing this protocol will first compute a context object that the other methods can use, instead of the raw arguments `first`, `after`, `last` and `before`.
26 | It is intended for scenarios where `totalCount`, `pageInfo` and `edges` all rely on very similar information.
27 | */
28 | public protocol ContextBasedConnection: OutputResolvable, ConcreteResolvable {
29 | /**
30 | Type of Item in the List. Has to conform to `OutputResolvable`
31 | */
32 | associatedtype Node
33 |
34 | /**
35 | Type of Edges in the List.
36 | Edges may contain additional information.
37 |
38 | If no edge type is provided, it will default to the Standard Implementation.
39 | */
40 | associatedtype Edge: EdgeProtocol = StandardEdge where Edge.Node == Node
41 |
42 | /**
43 | Context that describes some computations that your other methods need
44 | */
45 | associatedtype Context
46 |
47 | /**
48 | Converts the raw page arguments into the required context object.
49 |
50 | - Parameters:
51 | - first: Number of items that will be fetched at the start of the list
52 | - after: Cursor of the last element in the page that comes before. Signals that all items in the requested page should be strictly after the element with that cursor. The element with the cursor should be excluded
53 | - last: Number of items that will be fetched at the end of the list
54 | - before: Cursor of the first element in the page that comes after. Signals that all items in the requested page should be strictly before the element with that cursor. The element with the cursor should be excluded
55 | - eventLoop: Event Loop Group that can be used to create the futures
56 |
57 | - Returns: A future with the computed Context object
58 | */
59 | func context(first: Int?, after: String?, last: Int?, before: String?, eventLoop: EventLoopGroup) -> EventLoopFuture
60 |
61 | /**
62 | Computes the number of items in the list based on the context.
63 |
64 | - Parameters:
65 | - context: Precomputed Context
66 | - eventLoop: Event Loop Group that can be used to create the futures
67 |
68 | - Returns: A future with the computed count.
69 | */
70 | func totalCount(context: Context, eventLoop: EventLoopGroup) -> EventLoopFuture
71 |
72 | /**
73 | Computes information about the current page based on the context..
74 | This Information includes the start and end cursor and whether or not there's adjacent pages.
75 |
76 | - Parameters:
77 | - context: Precomputed Context
78 | - eventLoop: Event Loop Group that can be used to create the futures
79 |
80 | - Returns: A future with the computed Page Info
81 | */
82 | func pageInfo(context: Context, eventLoop: EventLoopGroup) -> EventLoopFuture
83 |
84 | /**
85 | Computes the edges for the current page
86 |
87 | - Parameters:
88 | - context: Precomputed Context
89 | - eventLoop: Event Loop Group that can be used to create the futures
90 |
91 | - Returns: A future with an array of edges.
92 | */
93 | func edges(context: Context, eventLoop: EventLoopGroup) -> EventLoopFuture<[Edge?]?>
94 | }
95 |
96 | extension ContextBasedConnection {
97 |
98 | public static var concreteTypeName: String {
99 | return "\(Node.concreteTypeName)\(String(describing: Self.self))"
100 | }
101 | /**
102 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
103 | */
104 | public static var additionalArguments: [String : InputResolvable.Type] {
105 | return ContextBasedConnectionWrapper.additionalArguments
106 | }
107 |
108 | /**
109 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
110 | */
111 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
112 | return try context.resolve(type: ContextBasedConnectionWrapper.self)
113 | }
114 |
115 | /**
116 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
117 | */
118 | public func resolve(source: Any, arguments: [String : Map], context: MutableContext, eventLoop: EventLoopGroup) throws -> Output {
119 | let first = try Optional.create(from: arguments["first"] ?? .null)
120 | let after = try Optional.create(from: arguments["after"] ?? .null)
121 | let last = try Optional.create(from: arguments["last"] ?? .null)
122 | let before = try Optional.create(from: arguments["before"] ?? .null)
123 |
124 | let connection = ContextBasedConnectionWrapper(connection: self,
125 | eventLoop: eventLoop,
126 | first: first,
127 | after: after,
128 | last: last,
129 | before: before)
130 |
131 | return .unsafeReceiver(connection)
132 | }
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/ContextBasedConnectionWrapper.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 | import GraphQL
5 | import ContextKit
6 |
7 | class ContextBasedConnectionWrapper {
8 | private let connection: Connection
9 | private let eventLoop: EventLoopGroup
10 |
11 | private var cachedContext: EventLoopFuture?
12 | private let first: Int?
13 | private let after: String?
14 | private let last: Int?
15 | private let before: String?
16 |
17 | init(connection: Connection,
18 | eventLoop: EventLoopGroup,
19 | first: Int?,
20 | after: String?,
21 | last: Int?,
22 | before: String?) {
23 |
24 | self.connection = connection
25 | self.eventLoop = eventLoop
26 | self.first = first
27 | self.after = after
28 | self.last = last
29 | self.before = before
30 | }
31 | }
32 |
33 | extension ContextBasedConnectionWrapper {
34 |
35 | private func context() -> EventLoopFuture {
36 | if let context = cachedContext {
37 | return context
38 | }
39 |
40 | let context = connection.context(first: first, after: after, last: last, before: before, eventLoop: eventLoop)
41 | cachedContext = context
42 | return context
43 | }
44 |
45 | private func pageInfo() -> EventLoopFuture {
46 | return context().flatMap { self.connection.pageInfo(context: $0, eventLoop: self.eventLoop) }
47 | }
48 |
49 | private func edges() -> EventLoopFuture<[Connection.Edge?]?> {
50 | return context().flatMap { self.connection.edges(context: $0, eventLoop: self.eventLoop) }
51 | }
52 |
53 | private func totalCount() -> EventLoopFuture {
54 | return context().flatMap { self.connection.totalCount(context: $0, eventLoop: self.eventLoop) }
55 | }
56 |
57 | }
58 |
59 | extension ContextBasedConnectionWrapper: ConcreteResolvable {
60 |
61 | static var concreteTypeName: String {
62 | return Connection.concreteTypeName
63 | }
64 |
65 | }
66 |
67 | extension ContextBasedConnectionWrapper: OutputResolvable {
68 |
69 | static var additionalArguments: [String : InputResolvable.Type] {
70 | return [
71 | "first" : Int?.self,
72 | "after" : String?.self,
73 | "last" : Int?.self,
74 | "before" : String?.self,
75 | ]
76 | }
77 |
78 | static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
79 | let fields = [
80 | "pageInfo" : GraphQLField(type: try context.reference(for: PageInfo.self)) { (receiver, args, context, eventLoop, _) -> Future in
81 | return try (receiver as! ContextBasedConnectionWrapper)
82 | .pageInfo()
83 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext, eventLoop: eventLoop)
84 | .convert(eventLoopGroup: eventLoop)
85 | },
86 | "edges" : GraphQLField(type: try context.reference(for: [Connection.Edge?]?.self)) { (receiver, args, context, eventLoop, _) -> Future in
87 | return try (receiver as! ContextBasedConnectionWrapper)
88 | .edges()
89 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext, eventLoop: eventLoop)
90 | .convert(eventLoopGroup: eventLoop)
91 | },
92 | "totalCount" : GraphQLField(type: try context.reference(for: Int.self)) { (receiver, args, context, eventLoop, _) -> Future in
93 | return try (receiver as! ContextBasedConnectionWrapper)
94 | .totalCount()
95 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext, eventLoop: eventLoop)
96 | .convert(eventLoopGroup: eventLoop)
97 | },
98 | ]
99 |
100 | return GraphQLNonNull(
101 | try GraphQLObjectType(name: concreteTypeName, fields: fields)
102 | )
103 | }
104 |
105 | func resolve(source: Any, arguments: [String : Map], context: MutableContext, eventLoop: EventLoopGroup) throws -> Output {
106 | return .unsafeReceiver(self)
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/EdgeProtocol.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | /**
5 | # EdgeProtocol
6 |
7 | Describes an Edge during Paging. An Edge contains the Node that is the value, the cursor of that value, and any additional information.
8 |
9 | ## Note
10 |
11 | An Edge is `OutputResolvable`. Any implementations of it, should be including the `node` and the `cursor` in the output.
12 | It is best if you delegate the implementation of `OutputResolvable` to `GraphQLObject` instead.
13 | */
14 | public protocol EdgeProtocol: OutputResolvable {
15 | associatedtype Node: OutputResolvable & ConcreteResolvable
16 |
17 | /**
18 | Value of the Edge
19 | */
20 | var node: Node? { get }
21 |
22 | /**
23 | Cursor to this value. Used to get a page before or after this value
24 | */
25 | var cursor: String { get }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/File.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Mathias.Quintero on 5/28/20.
6 | //
7 |
8 | import Foundation
9 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/FixedPageSizeIndexedConnection.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 |
5 | public protocol FixedPageSizeIndexedConnection: IndexedConnection {
6 | func pageSize(eventLoop: EventLoopGroup) -> EventLoopFuture
7 | func page(at index: Int, eventLoop: EventLoopGroup) -> EventLoopFuture<[Node?]>
8 | }
9 |
10 | extension FixedPageSizeIndexedConnection {
11 |
12 | public func defaultPageSize(eventLoop: EventLoopGroup) -> EventLoopFuture {
13 | return pageSize(eventLoop: eventLoop).map(Optional.some)
14 | }
15 |
16 | public func nodes(offset: Int, size: Int, eventLoop: EventLoopGroup) -> EventLoopFuture<[Node?]?> {
17 | return totalCount(eventLoop: eventLoop)
18 | .and(pageSize(eventLoop: eventLoop))
19 | .flatMap { totalCount, pageSize in
20 | let end = offset + size
21 | let firstPage = offset / pageSize
22 | let lastPage = (end - 1) / pageSize
23 | let pages = firstPage...lastPage
24 |
25 | let skipOnFirst = offset.quotientAndRemainder(dividingBy: pageSize).remainder
26 | let skipOnLast = min(
27 | (pageSize - end.quotientAndRemainder(dividingBy: pageSize).remainder).quotientAndRemainder(dividingBy: pageSize).remainder,
28 | totalCount - end
29 | )
30 |
31 | let futures = pages
32 | .map { self.page(at: $0, eventLoop: eventLoop) }
33 |
34 | return EventLoopFuture
35 | .whenAllSucceed(futures, on: eventLoop.next())
36 | .map { $0.flatMap { $0 } }
37 | .map { $0.dropFirst(skipOnFirst).dropLast(skipOnLast) }
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/IndexConnectionWrapper.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 |
5 | private let lock = Lock()
6 | private var translators: [ConnectionIdentifier : IndexCursorTranslator] = [:]
7 |
8 | private func withTranslator(for connectionIdentifier: ConnectionIdentifier,
9 | using type: IndexCursorTranslator.Type,
10 | perform: (inout IndexCursorTranslator) throws -> T) rethrows -> T {
11 |
12 | return try lock.withLock {
13 | return try perform(&translators[connectionIdentifier, default: type.init()])
14 | }
15 | }
16 |
17 | private func withTranslator(for connectionIdentifier: ConnectionIdentifier,
18 | using type: IndexCursorTranslator.Type,
19 | perform: (IndexCursorTranslator) throws -> T) rethrows -> T {
20 |
21 | let translator = lock.withLock {
22 | return translators[connectionIdentifier, default: type.init()]
23 | }
24 | return try perform(translator)
25 | }
26 |
27 | private struct ConnectionIdentifier: Hashable {
28 | let type: Int
29 | let connection: AnyHashable
30 | }
31 |
32 | struct IndexedConnectionWrapper: ContextBasedConnection, ConcreteResolvable {
33 | static var concreteTypeName: String {
34 | return "\(Node.concreteTypeName)Connection"
35 | }
36 |
37 | typealias Node = Connection.Node
38 |
39 | struct Context {
40 | let totalCount: Int
41 | let offset: Int
42 | let size: Int
43 | }
44 |
45 | private let connection: Connection
46 | private let identifier: ConnectionIdentifier
47 |
48 | init(connection: Connection) {
49 | self.connection = connection
50 | self.identifier = ConnectionIdentifier(type: unsafeBitCast(Connection.self as Any.Type, to: Int.self),
51 | connection: AnyHashable(connection.identifier))
52 | }
53 |
54 | private func cursor(for index: Int) throws -> String {
55 | return try withTranslator(for: identifier, using: Connection.Translator.self) { translator in
56 | return try translator.cursor(for: index)
57 | }
58 | }
59 |
60 | private func index(for cursor: String) throws -> Int {
61 | return try withTranslator(for: identifier, using: Connection.Translator.self) { translator in
62 | return try translator.index(for: cursor)
63 | }
64 | }
65 |
66 | func context(first: Int?, after: String?, last: Int?, before: String?, eventLoop: EventLoopGroup) -> EventLoopFuture {
67 | return connection
68 | .totalCount(eventLoop: eventLoop)
69 | .and(connection.defaultPageSize(eventLoop: eventLoop))
70 | .flatMapThrowing { totalCount, defaultPageSize in
71 | let perPage = defaultPageSize ?? totalCount
72 |
73 | if perPage == 0 {
74 | return Context(totalCount: totalCount, offset: 0, size: 0)
75 | }
76 |
77 | let first = first == nil && last == nil ? perPage : first
78 | var start = 0
79 | var end = totalCount
80 |
81 | if let after = try after.map(self.index(for:)) {
82 | start = max(start, after + 1)
83 | }
84 |
85 | if let before = try before.map(self.index(for:)) {
86 | end = min(before, end)
87 | }
88 |
89 | if let first = first {
90 | end = min(start + first, end)
91 | }
92 |
93 | if let last = last {
94 | start = max(start, end - last)
95 | }
96 |
97 | return Context(totalCount: totalCount, offset: start, size: max(end - start, 0))
98 | }
99 | }
100 |
101 | func totalCount(context: Context, eventLoop: EventLoopGroup) -> EventLoopFuture {
102 | return eventLoop.next().makeSucceededFuture(context.totalCount)
103 | }
104 |
105 | func pageInfo(context: Context, eventLoop: EventLoopGroup) -> EventLoopFuture {
106 | return eventLoop.next().submit {
107 | let hasNextPage = context.offset + context.size < context.totalCount
108 | let hasPreviousPage = context.offset > 0
109 |
110 | return PageInfo(hasNextPage: hasNextPage,
111 | hasPreviousPage: hasPreviousPage,
112 | startCursor: try self.cursor(for: context.offset),
113 | endCursor: try self.cursor(for: context.offset + context.size - 1))
114 | }
115 | }
116 |
117 | func edges(context: Context, eventLoop: EventLoopGroup) -> EventLoopFuture<[StandardEdge?]?> {
118 | if context.size < 1 {
119 | return eventLoop.next().makeSucceededFuture([])
120 | }
121 |
122 | return connection
123 | .nodes(offset: context.offset, size: context.size, eventLoop: eventLoop)
124 | .flatMapThrowing { nodes in
125 | try nodes?.enumerated().map { StandardEdge(node: $0.element, cursor: try self.cursor(for: context.offset + $0.offset)) }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/IndexCursorTranslator.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | public protocol IndexCursorTranslator {
5 | init()
6 | mutating func cursor(for index: Int) throws -> String
7 | mutating func index(for cursor: String) throws -> Int
8 | }
9 |
10 | public struct StandardIndexCursorTranslator: IndexCursorTranslator {
11 | private var cursors: [String : Int] = [:]
12 |
13 | enum Error: Swift.Error {
14 | case unrecognizedCursor(String, for: StandardIndexCursorTranslator)
15 | }
16 |
17 | public init() { }
18 |
19 | public mutating func cursor(for index: Int) throws -> String {
20 | var string = index.obfuscate()
21 |
22 | // Remove clashes
23 | while let clashingIndex = cursors[string], clashingIndex != index {
24 | string = string.obfuscate()
25 | }
26 |
27 | if cursors[string] == nil {
28 | cursors[string] = index
29 | }
30 |
31 | return string
32 | }
33 |
34 | public func index(for cursor: String) throws -> Int {
35 | guard let index = cursors[cursor] else {
36 | throw Error.unrecognizedCursor(cursor, for: self)
37 | }
38 | return index
39 | }
40 | }
41 |
42 | extension Hashable {
43 |
44 | fileprivate func obfuscate() -> String {
45 | var hasher = Hasher()
46 | hash(into: &hasher)
47 | return withUnsafeBytes(of: hasher.finalize()) { Data(buffer: $0.bindMemory(to: UInt8.self)).base64EncodedString() }
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/IndexedConnection.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 | import GraphQL
5 | import ContextKit
6 |
7 | public protocol IndexedConnection: OutputResolvable, ConcreteResolvable {
8 | associatedtype Node: ConcreteResolvable & OutputResolvable
9 | associatedtype Identifier: Hashable
10 | associatedtype Translator: IndexCursorTranslator = StandardIndexCursorTranslator
11 |
12 | var identifier: Identifier { get }
13 |
14 | func totalCount(eventLoop: EventLoopGroup) -> EventLoopFuture
15 | func defaultPageSize(eventLoop: EventLoopGroup) -> EventLoopFuture
16 | func nodes(offset: Int, size: Int, eventLoop: EventLoopGroup) -> EventLoopFuture<[Node?]?>
17 | }
18 |
19 | extension IndexedConnection {
20 |
21 | public func defaultPageSize(eventLoop: EventLoopGroup) -> EventLoopFuture {
22 | return eventLoop.next().makeSucceededFuture(nil)
23 | }
24 |
25 | }
26 |
27 | extension IndexedConnection {
28 |
29 | public static var concreteTypeName: String {
30 | return IndexedConnectionWrapper.concreteTypeName
31 | }
32 | /**
33 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
34 | */
35 | public static var additionalArguments: [String : InputResolvable.Type] {
36 | return IndexedConnectionWrapper.additionalArguments
37 | }
38 |
39 | /**
40 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
41 | */
42 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
43 | return try context.resolve(type: IndexedConnectionWrapper.self)
44 | }
45 |
46 | /**
47 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
48 | */
49 | public func resolve(source: Any, arguments: [String : Map], context: MutableContext, eventLoop: EventLoopGroup) throws -> Output {
50 | try IndexedConnectionWrapper(connection: self).resolve(source: source, arguments: arguments, context: context, eventLoop: eventLoop)
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/PageInfo.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | /**
5 | # Page Info
6 |
7 | This is the information about the current Page.
8 | This Information includes the start and end cursor and whether or not there's adjacent pages.
9 | */
10 | public class PageInfo: GraphQLObject {
11 | let hasNextPage: Bool
12 | let hasPreviousPage: Bool
13 |
14 | let startCursor: String?
15 | let endCursor: String?
16 |
17 | /**
18 | Initializer for Page Info
19 |
20 | - Parameters:
21 | - hasNextPage: is there a page after this one
22 | - hasPreviousPage: is there a page before this one
23 | - startCursor: What's the cursor of the first item. (`nil` if empty)
24 | - endCursor: What's the cursor of the last item. (`nil` if empty)
25 | */
26 | public init(hasNextPage: Bool,
27 | hasPreviousPage: Bool,
28 | startCursor: String?,
29 | endCursor: String?) {
30 |
31 | self.hasNextPage = hasNextPage
32 | self.hasPreviousPage = hasPreviousPage
33 | self.startCursor = startCursor
34 | self.endCursor = endCursor
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Connection/StandardEdge.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 | import GraphQL
5 | import ContextKit
6 |
7 | /**
8 | # Standard Edge
9 |
10 | This is the default implementation of an Edge. It has the node and cursor and nothing else.
11 | */
12 | public struct StandardEdge: EdgeProtocol, ConcreteResolvable {
13 | public let node: Node?
14 | public let cursor: String
15 |
16 | /**
17 | Initializer for an Edge
18 |
19 | - Parameters:
20 | - node: Value the edge is pointing to
21 | - cursor: Cursor for this Value
22 | */
23 | public init(node: Node?, cursor: String) {
24 | self.node = node
25 | self.cursor = cursor
26 | }
27 | }
28 |
29 | extension StandardEdge {
30 | public static var concreteTypeName: String {
31 | return "\(Node.concreteTypeName)Edge"
32 | }
33 | }
34 |
35 | extension StandardEdge {
36 |
37 | public static var additionalArguments: [String : InputResolvable.Type] {
38 | return Node?.additionalArguments
39 | }
40 |
41 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
42 | let fields = [
43 | "node" : GraphQLField(type: try context.reference(for: Optional.self)) { (receiver, args, context, eventLoop, _) -> Future in
44 | return try (receiver as! StandardEdge)
45 | .node
46 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext, eventLoop: eventLoop)
47 | .convert(eventLoopGroup: eventLoop)
48 | },
49 | "cursor" : GraphQLField(type: try context.reference(for: String.self)) { (receiver, args, context, eventLoop, _) -> Future in
50 | return try (receiver as! StandardEdge)
51 | .cursor
52 | .resolve(source: receiver, arguments: try args.dictionaryValue(), context: context as! MutableContext,eventLoop: eventLoop)
53 | .convert(eventLoopGroup: eventLoop)
54 | },
55 | ]
56 |
57 | return GraphQLNonNull(
58 | try GraphQLObjectType(name: concreteTypeName, fields: fields)
59 | )
60 | }
61 |
62 | public func resolve(source: Any, arguments: [String : Map], context: MutableContext, eventLoop: EventLoopGroup) throws -> Output {
63 | return .unsafeReceiver(self)
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Future+OutputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import NIO
5 | import ContextKit
6 |
7 | /**
8 | # Conditional Conformance
9 |
10 | All Futures of values that are GraphQL Outputs, can be outputs themselves
11 | */
12 | extension EventLoopFuture: OutputResolvable where Value: OutputResolvable {
13 |
14 | public static var additionalArguments: [String : InputResolvable.Type] {
15 | return Value.additionalArguments
16 | }
17 |
18 | public static func reference(using context: inout Resolution.Context) throws -> GraphQLOutputType {
19 | return try context.reference(for: Value.self)
20 | }
21 |
22 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
23 | return try context.resolve(type: Value.self)
24 | }
25 |
26 | public func resolve(source: Any, arguments: [String : Map], context: MutableContext, eventLoop: EventLoopGroup) throws -> Output {
27 | let future = flatMapThrowing { try $0.resolve(source: source, arguments: arguments, context: context, eventLoop: eventLoop) }
28 | return .future(future)
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/GraphQLObject+OutputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import NIO
5 | import ContextKit
6 |
7 | extension GraphQLObject {
8 |
9 | /**
10 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
11 | */
12 | public static var additionalArguments: [String : InputResolvable.Type] {
13 | return [:]
14 | }
15 |
16 | /**
17 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
18 | */
19 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
20 | return GraphQLNonNull(try resolveObject(using: &context))
21 | }
22 |
23 | /**
24 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
25 | */
26 | public func resolve(source: Any,
27 | arguments: [String : Map],
28 | context: MutableContext,
29 | eventLoop: EventLoopGroup) throws -> Output {
30 |
31 | return .object(self)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/GraphQLObject+resolve.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import Runtime
5 | import NIO
6 |
7 | extension GraphQLObject {
8 |
9 | static func resolveObject(using context: inout Resolution.Context) throws -> GraphQLObjectType {
10 | let inheritance = try typeInfo(of: Self.self, .inheritance)
11 | let fieldsAndInterfaces = try self.fieldsAndInterfaces(using: &context)
12 |
13 | let interfaces = try inheritance
14 | .compactMap { $0 as? GraphQLObject.Type }
15 | .map { try context.resolveInterface(object: $0) } + fieldsAndInterfaces.interfaces
16 |
17 | let type = try GraphQLObjectType(name: concreteTypeName,
18 | fields: fieldsAndInterfaces.fields,
19 | interfaces: interfaces.distinct(by: \.name)) { value, _, _ in value is Self }
20 |
21 | return type
22 | }
23 |
24 | static func fieldsAndInterfaces(using context: inout Resolution.Context) throws -> FieldsAndInterfaces {
25 | let (typeProperties, typeMethods) = try typeInfo(of: Self.self, .properties, .methods)
26 |
27 | let isNode: Bool
28 | if let type = Self.self as? Node.Type {
29 | context.append(type: type)
30 | isNode = true
31 | } else {
32 | isNode = false
33 | }
34 | let nodeFields = isNode ? ["id" : GraphQLNodeIdField] : [:]
35 |
36 | let gettersThatShouldBeIgnored = Set(typeProperties.filter { $0.type is CustomGraphQLProperty.Type }.map { $0.name.deleting(prefix: "_") })
37 | let propertyResults = try typeProperties.compactMap { try $0.resolve(for: Self.self, using: &context) }
38 | let properties = propertyResults.reduce([:]) { dictionary, result in
39 | return dictionary.merging(result.fieldMap) { first, _ in first }
40 | }
41 |
42 | let methodMap = Dictionary(typeMethods.map { ($0.methodName.deleting(prefix: "$"), $0) }) { first, _ in first }
43 | let methods = try methodMap.filter { !gettersThatShouldBeIgnored.contains($0.key) }.compactMapValues { try $0.resolve(for: Self.self, using: &context) }
44 |
45 | let fields = properties
46 | .merging(methods) { $1 }
47 | .merging(nodeFields) { $1 }
48 |
49 | return FieldsAndInterfaces(interfaces: propertyResults.flatMap(\.interfaces) + [isNode ? GraphQLNode : nil].compactMap { $0 }, fields: fields)
50 | }
51 |
52 | }
53 |
54 | struct FieldsAndInterfaces {
55 | let interfaces: [GraphQLInterfaceType]
56 | let fields: GraphQLFieldMap
57 | }
58 |
59 | extension Sequence {
60 |
61 | fileprivate func distinct(by id: (Element) -> T) -> [Element] {
62 | var set = Set()
63 | var result = [Element]()
64 | for element in self {
65 | let elementId = id(element)
66 | if !set.contains(elementId) {
67 | set.formUnion([elementId])
68 | result.append(element)
69 | }
70 | }
71 | return result
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/GraphQLObject.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | /**
5 | # GraphQLObject
6 |
7 | A GraphQL Object is a Class that can be used as an output in GraphQL.
8 |
9 | It behaves as follows:
10 | - All properties of types that are `OutputResolvable` (can be outputs in GraphQL) will be added to the Schema.
11 | - All methods where all arguments are of types that are `InputResolvable` (can be inputs in GraphQL) and the return type is `OutputResolvable` (can be outputs in GraphQL) will be added to the Schema.
12 |
13 | ## Example
14 |
15 | If we are implementing a Todo App, a Todo and User in our API would be two classes:
16 |
17 | ```swift
18 | class Author: GraphQLObject {
19 | var name: String
20 |
21 | func store() { ... }
22 |
23 | func todos() -> [Todo] { ... }
24 | }
25 |
26 | class Todo: GraphQLObject {
27 | var title: String
28 | var completed: Bool
29 |
30 | func store() { ... }
31 |
32 | func author() -> User { ... }
33 | }
34 | ```
35 |
36 | The previous code would then be exported to GraphQL like so:
37 |
38 | ```graphql
39 | type User {
40 | name: String!
41 | todos: [Todo!]!
42 | }
43 |
44 | type Todo {
45 | title: String!
46 | completed: Boolean!
47 | author: User!
48 | }
49 | ```
50 |
51 | As can be seen in the example:
52 | - All the properties are scalar values that can be outputs in GraphQL.
53 | - All the properties are included.
54 | - And all the functions that return GraphQL Values are also included
55 | - The store functions return `Void`, which is not a GraphQL Output. These methods are therefore not included.
56 | */
57 | public protocol GraphQLObject : AnyObject, OutputResolvable, ConcreteResolvable, KeyPathListable {
58 | /**
59 | Loads object from source.
60 |
61 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
62 |
63 | - Parameters:
64 | - source: In most cases it's just an instance of `Self`, except with Root Types, where it's an instance of the `ViewerContext`
65 |
66 | - Returns: the object that will be used for method dispatching
67 | */
68 | static func object(from source: Any) -> AnyObject
69 | }
70 |
71 | extension GraphQLObject {
72 |
73 | /**
74 | - Warning: default implementation from `GraphZahl`. Do not override unless you know exactly what you are doing.
75 | */
76 | public static func object(from source: Any) -> AnyObject {
77 | return source as AnyObject
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/PropertyWrappers/CustomGraphQLProperty.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import Runtime
4 |
5 | protocol CustomGraphQLProperty {
6 | static func resolve(with property: PropertyInfo,
7 | for receiverType: GraphQLObject.Type,
8 | using context: inout Resolution.Context) throws -> PropertyResult
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/PropertyWrappers/Ignore.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import Runtime
4 |
5 | @propertyWrapper
6 | public struct Ignore {
7 | public var wrappedValue: T
8 |
9 | public init(wrappedValue: T) {
10 | self.wrappedValue = wrappedValue
11 | }
12 | }
13 |
14 | extension Ignore: CustomGraphQLProperty {
15 |
16 | static func resolve(with property: PropertyInfo,
17 | for receiverType: GraphQLObject.Type,
18 | using context: inout Resolution.Context) throws -> PropertyResult {
19 | return .ignore
20 | }
21 |
22 | }
23 |
24 | extension Ignore: Encodable where T: Encodable {
25 |
26 | public func encode(to encoder: Encoder) throws {
27 | try wrappedValue.encode(to: encoder)
28 | }
29 |
30 | }
31 |
32 | extension Ignore: Decodable where T: Decodable {
33 |
34 | public init(from decoder: Decoder) throws {
35 | self.init(wrappedValue: try T(from: decoder))
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/PropertyWrappers/Inline.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import Runtime
5 |
6 | @propertyWrapper
7 | public struct Inline {
8 | public var wrappedValue: Wrapped
9 |
10 | public init(wrappedValue: Wrapped) {
11 | self.wrappedValue = wrappedValue
12 | }
13 | }
14 |
15 | extension Inline: CustomGraphQLProperty {
16 |
17 | static func resolve(with property: PropertyInfo,
18 | for receiverType: GraphQLObject.Type,
19 | using context: inout Resolution.Context) throws -> PropertyResult {
20 |
21 | let fields = try Wrapped.fieldsAndInterfaces(using: &context).fields.mapValues { field in
22 | return GraphQLField(
23 | type: field.type,
24 | description: field.description,
25 | deprecationReason: field.deprecationReason,
26 | args: field.args
27 | ) { source, arguments, context, eventLoop, info in
28 | let object = receiverType.object(from: source)
29 | let result = try property.get(from: object) as! Self
30 | return try field.resolve!(result.wrappedValue, arguments, context, eventLoop, info)
31 | }
32 | }
33 |
34 | return .interfaces([], fields: fields)
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/PropertyWrappers/InlineAsInterface.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import Runtime
5 |
6 | @propertyWrapper
7 | public struct InlineAsInterface {
8 | public var wrappedValue: Wrapped
9 |
10 | public init(wrappedValue: Wrapped) {
11 | self.wrappedValue = wrappedValue
12 | }
13 | }
14 |
15 | extension InlineAsInterface: CustomGraphQLProperty {
16 |
17 | static func resolve(with property: PropertyInfo,
18 | for receiverType: GraphQLObject.Type,
19 | using context: inout Resolution.Context) throws -> PropertyResult {
20 |
21 | let object = try context.resolve(object: Wrapped.self)
22 | let interface = try context.resolveInterface(object: Wrapped.self)
23 |
24 | let fields = object.fields.mapValues { field in
25 | return GraphQLField(
26 | type: field.type,
27 | description: field.description,
28 | deprecationReason: field.deprecationReason,
29 | args: Dictionary(
30 | uniqueKeysWithValues: field.args.map { ($0.name, $0.type) }
31 | )
32 | .mapValues { GraphQLArgument(type: $0) }
33 | ) { source, arguments, context, eventLoop, info in
34 |
35 | let object = receiverType.object(from: source)
36 | let result = try property.get(from: object) as! Self
37 | return try field.resolve!(result.wrappedValue, arguments, context, eventLoop, info)
38 | }
39 | }
40 |
41 | return .interfaces([interface] + object.interfaces, fields: fields)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/PropertyWrappers/LazyInline.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import Runtime
5 | import NIO
6 |
7 | @propertyWrapper
8 | public class LazyInline {
9 | private let load: () -> EventLoopFuture
10 | fileprivate lazy var future: EventLoopFuture = {
11 | return load()
12 | }()
13 |
14 | public var wrappedValue: Wrapped {
15 | return try! future.wait()
16 | }
17 |
18 | public init(_ load: @escaping () -> EventLoopFuture) {
19 | self.load = load
20 | }
21 | }
22 |
23 | extension LazyInline: CustomGraphQLProperty {
24 |
25 | static func resolve(with property: PropertyInfo,
26 | for receiverType: GraphQLObject.Type,
27 | using context: inout Resolution.Context) throws -> PropertyResult {
28 |
29 | let fields = try Wrapped.fieldsAndInterfaces(using: &context).fields.mapValues { field in
30 | return GraphQLField(
31 | type: field.type,
32 | description: field.description,
33 | deprecationReason: field.deprecationReason,
34 | args: field.args
35 | ) { source, arguments, context, eventLoop, info in
36 |
37 | let object = receiverType.object(from: source)
38 | let result = try property.get(from: object) as! Self
39 | return result
40 | .future
41 | .flatMapThrowing { try field.resolve!($0, arguments, context, eventLoop, info) }
42 | .flatMap { $0 }
43 | }
44 | }
45 |
46 | return .interfaces([], fields: fields)
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/PropertyWrappers/LazyInlineAsInterface.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import Runtime
5 | import NIO
6 |
7 | @propertyWrapper
8 | public class LazyInlineAsInterface {
9 | private let load: () -> EventLoopFuture
10 | fileprivate lazy var future: EventLoopFuture = {
11 | return load()
12 | }()
13 |
14 | public var wrappedValue: Wrapped {
15 | return try! future.wait()
16 | }
17 |
18 | public init(_ load: @escaping () -> EventLoopFuture) {
19 | self.load = load
20 | }
21 | }
22 |
23 | extension LazyInlineAsInterface: CustomGraphQLProperty {
24 |
25 | static func resolve(with property: PropertyInfo,
26 | for receiverType: GraphQLObject.Type,
27 | using context: inout Resolution.Context) throws -> PropertyResult {
28 |
29 | let object = try context.resolve(object: Wrapped.self)
30 | let interface = try context.resolveInterface(object: Wrapped.self)
31 |
32 | let fields = object.fields.mapValues { field in
33 | return GraphQLField(
34 | type: field.type,
35 | description: field.description,
36 | deprecationReason: field.deprecationReason,
37 | args: Dictionary(
38 | uniqueKeysWithValues: field.args.map { ($0.name, $0.type) }
39 | )
40 | .mapValues { GraphQLArgument(type: $0) }
41 | ) { source, arguments, context, eventLoop, info in
42 |
43 | let object = receiverType.object(from: source)
44 | let result = try property.get(from: object) as! Self
45 | return result
46 | .future
47 | .flatMapThrowing { try field.resolve!($0, arguments, context, eventLoop, info) }
48 | .flatMap { $0 }
49 | }
50 | }
51 |
52 | return .interfaces([interface] + object.interfaces, fields: fields)
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Object/PropertyWrappers/PropertyResult.swift:
--------------------------------------------------------------------------------
1 |
2 | import GraphQL
3 |
4 | enum PropertyResult {
5 | case field(String, GraphQLField)
6 | case interfaces([GraphQLInterfaceType], fields: [String : GraphQLField])
7 | case ignore
8 | }
9 |
10 | extension PropertyResult {
11 |
12 | var fieldMap: [String : GraphQLField] {
13 | switch self {
14 | case .field(let name, let field):
15 | return [name : field]
16 | case .interfaces(_, let fields):
17 | return fields
18 | case .ignore:
19 | return [:]
20 | }
21 | }
22 |
23 | var interfaces: [GraphQLInterfaceType] {
24 | guard case .interfaces(let interface, _) = self else { return [] }
25 | return interface
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Implementations/Optional+OutputResolvable.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import NIO
5 | import ContextKit
6 |
7 | /**
8 | # Conditional Conformance
9 |
10 | All optionals of values that are GraphQL Outputs, can be outputs themselves
11 | */
12 | extension Optional: OutputResolvable where Wrapped: OutputResolvable {
13 |
14 | public static var additionalArguments: [String : InputResolvable.Type] {
15 | return Wrapped.additionalArguments
16 | }
17 |
18 | public static func reference(using context: inout Resolution.Context) throws -> GraphQLOutputType {
19 | switch try context.reference(for: Wrapped.self) {
20 | case let resolved as GraphQLNonNull:
21 | guard let type = resolved.ofType as? GraphQLOutputType else { fatalError() }
22 | return type
23 | case let type:
24 | return type
25 | }
26 | }
27 |
28 | public static func resolve(using context: inout Resolution.Context) throws -> GraphQLOutputType {
29 | switch try context.resolve(type: Wrapped.self) {
30 | case let resolved as GraphQLNonNull:
31 | guard let type = resolved.ofType as? GraphQLOutputType else { fatalError() }
32 | return type
33 | case let type:
34 | return type
35 | }
36 | }
37 |
38 | public func resolve(source: Any,
39 | arguments: [String : Map],
40 | context: MutableContext,
41 | eventLoop: EventLoopGroup) throws -> Output {
42 |
43 | guard let value = self else { return .map(.null) }
44 | return try value.resolve(source: source, arguments: arguments, context: context, eventLoop: eventLoop)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Node.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import NIO
4 | import ContextKit
5 | import GraphQL
6 |
7 | public protocol Node: GraphQLObject {
8 | func id(context: MutableContext, eventLoop: EventLoopGroup) -> EventLoopFuture
9 |
10 | static func find(id: String, context: MutableContext, eventLoop: EventLoopGroup ) -> EventLoopFuture
11 | }
12 |
13 | let GraphQLNodeIdField: GraphQLField = {
14 | return GraphQLField(type: GraphQLNonNull(GraphQLID),
15 | description: "The id of the object",
16 | deprecationReason: nil,
17 | args: [:]) { (source, _, context, eventLoop, _) -> Future in
18 |
19 | return (source as! Node).id(context: context as! MutableContext, eventLoop: eventLoop).map { $0 }
20 | }
21 | }()
22 |
23 | let GraphQLNode: GraphQLInterfaceType = {
24 | let fields = [
25 | "id" : GraphQLNodeIdField
26 | ]
27 | return try! GraphQLInterfaceType(name: "Node", fields: fields, resolveType: nil)
28 | }()
29 |
--------------------------------------------------------------------------------
/Sources/GraphZahl/Resolution/OutputResolvable/Output.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GraphQL
4 | import NIO
5 |
6 | public indirect enum Output {
7 | case map(Map)
8 | case array([Output])
9 | case future(EventLoopFuture