From 63b4e5865eea93e5c8b5bed3e33716a15a4a21d6 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 23 Sep 2021 13:45:07 -0700 Subject: [PATCH 1/5] Kill "sendOperationIdentifiers" option on RequestBodyCreator --- Sources/Apollo/JSONRequest.swift | 9 ++------- Sources/Apollo/RequestBodyCreator.swift | 11 ----------- Sources/Apollo/UploadRequest.swift | 3 --- Sources/ApolloWebSocket/WebSocketTransport.swift | 5 ----- Tests/ApolloTests/GETTransformerTests.swift | 4 ---- Tests/ApolloTests/RequestBodyCreatorTests.swift | 3 +-- Tests/ApolloTests/TestCustomRequestBodyCreator.swift | 1 - Tests/ApolloTests/UploadRequestTests.swift | 6 +++--- 8 files changed, 6 insertions(+), 36 deletions(-) diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index d3addc00de..24f9b7ed0f 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -54,10 +54,6 @@ open class JSONRequest: HTTPRequest { cachePolicy: cachePolicy) } - open var sendOperationIdentifier: Bool { - self.operation.operationIdentifier != nil - } - open override func toURLRequest() throws -> URLRequest { var request = try super.toURLRequest() @@ -91,9 +87,8 @@ open class JSONRequest: HTTPRequest { } let body = self.requestBodyCreator.requestBody(for: operation, - sendOperationIdentifiers: self.sendOperationIdentifier, - sendQueryDocument: sendQueryDocument, - autoPersistQuery: autoPersistQueries) + sendQueryDocument: sendQueryDocument, + autoPersistQuery: autoPersistQueries) let httpMethod: GraphQLHTTPMethod = useGetMethod ? .GET : .POST switch httpMethod { diff --git a/Sources/Apollo/RequestBodyCreator.swift b/Sources/Apollo/RequestBodyCreator.swift index 9d1cc66287..c5c2cbbab0 100644 --- a/Sources/Apollo/RequestBodyCreator.swift +++ b/Sources/Apollo/RequestBodyCreator.swift @@ -9,13 +9,11 @@ public protocol RequestBodyCreator { /// /// - Parameters: /// - operation: The operation to use - /// - sendOperationIdentifiers: Whether or not to send operation identifiers. Should default to `false`. /// - sendQueryDocument: Whether or not to send the full query document. Should default to `true`. /// - autoPersistQuery: Whether to use auto-persisted query information. Should default to `false`. /// - Returns: The created `GraphQLMap` func requestBody( for operation: Operation, - sendOperationIdentifiers: Bool, sendQueryDocument: Bool, autoPersistQuery: Bool ) -> JSONEncodableDictionary @@ -27,7 +25,6 @@ extension RequestBodyCreator { public func requestBody( for operation: Operation, - sendOperationIdentifiers: Bool, sendQueryDocument: Bool, autoPersistQuery: Bool ) -> JSONEncodableDictionary { @@ -39,14 +36,6 @@ extension RequestBodyCreator { body["variables"] = variables.jsonEncodableObject } - if sendOperationIdentifiers { - guard let operationIdentifier = operation.operationIdentifier else { - preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers") - } - - body["id"] = operationIdentifier - } - if sendQueryDocument { body["query"] = operation.queryDocument } diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index a2767e63a4..ba2fd216a0 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -58,8 +58,6 @@ open class UploadRequest: HTTPRequest { /// - Throws: Any error arising from creating the form data /// - Returns: The created form data open func requestMultipartFormData() throws -> MultipartFormData { - let shouldSendOperationID = (self.operation.operationIdentifier != nil) - let formData: MultipartFormData if let boundary = manualBoundary { @@ -72,7 +70,6 @@ open class UploadRequest: HTTPRequest { // for the files in the rest of the form data let fieldsForFiles = Set(files.map { $0.fieldName }).sorted() var fields = self.requestBodyCreator.requestBody(for: operation, - sendOperationIdentifiers: shouldSendOperationID, sendQueryDocument: true, autoPersistQuery: false) var variables = fields["variables"] as? JSONEncodableDictionary ?? JSONEncodableDictionary() diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 2fdd384766..576fc9074a 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -84,8 +84,6 @@ public class WebSocketTransport { public fileprivate(set) var clientName: String /// The client version to use for this client. Defaults to `Self.defaultClientVersion`. public fileprivate(set) var clientVersion: String - /// Whether or not to send operation identifiers with operations. Defaults to false. - public let sendOperationIdentifiers: Bool /// Whether to auto reconnect when websocket looses connection. Defaults to true. public let reconnect: Atomic /// How long to wait before attempting to reconnect. Defaults to half a second. @@ -108,7 +106,6 @@ public class WebSocketTransport { public init( clientName: String = WebSocketTransport.defaultClientName, clientVersion: String = WebSocketTransport.defaultClientVersion, - sendOperationIdentifiers: Bool = false, reconnect: Bool = true, reconnectionInterval: TimeInterval = 0.5, allowSendingDuplicates: Bool = true, @@ -119,7 +116,6 @@ public class WebSocketTransport { ) { self.clientName = clientName self.clientVersion = clientVersion - self.sendOperationIdentifiers = sendOperationIdentifiers self.reconnect = Atomic(reconnect) self.reconnectionInterval = reconnectionInterval self.allowSendingDuplicates = allowSendingDuplicates @@ -299,7 +295,6 @@ public class WebSocketTransport { func sendHelper(operation: Operation, resultHandler: @escaping (_ result: Result) -> Void) -> String? { let body = config.requestBodyCreator .requestBody(for: operation, - sendOperationIdentifiers: self.config.sendOperationIdentifiers, sendQueryDocument: true, autoPersistQuery: false) let identifier = config.operationMessageIdCreator.requestId() diff --git a/Tests/ApolloTests/GETTransformerTests.swift b/Tests/ApolloTests/GETTransformerTests.swift index be5adc62f0..a1ea8d40e4 100644 --- a/Tests/ApolloTests/GETTransformerTests.swift +++ b/Tests/ApolloTests/GETTransformerTests.swift @@ -40,7 +40,6 @@ query MockQuery($param: String) { operation.variables = ["param": "TestParamValue"] let body = requestBodyCreator.requestBody(for: operation, - sendOperationIdentifiers: false, sendQueryDocument: true, autoPersistQuery: false) @@ -67,7 +66,6 @@ query MockQuery($param: MockEnum) { operation.variables = ["param": MockEnum.LARGE] let body = requestBodyCreator.requestBody(for: operation, - sendOperationIdentifiers: false, sendQueryDocument: true, autoPersistQuery: false) @@ -94,7 +92,6 @@ query MockQuery($a: String, $b: Boolean!) { operation.variables = ["a": "TestParamValue", "b": true] let body = requestBodyCreator.requestBody(for: operation, - sendOperationIdentifiers: false, sendQueryDocument: true, autoPersistQuery: false) @@ -218,7 +215,6 @@ query MockQuery($param: String) { operation.variables = ["param": GraphQLNullable.null] let body = requestBodyCreator.requestBody(for: operation, - sendOperationIdentifiers: false, sendQueryDocument: true, autoPersistQuery: false) diff --git a/Tests/ApolloTests/RequestBodyCreatorTests.swift b/Tests/ApolloTests/RequestBodyCreatorTests.swift index e01d6cf3d5..e35623c4f7 100644 --- a/Tests/ApolloTests/RequestBodyCreatorTests.swift +++ b/Tests/ApolloTests/RequestBodyCreatorTests.swift @@ -18,8 +18,7 @@ class RequestBodyCreatorTests: XCTestCase { with creator: RequestBodyCreator, for operation: Operation ) -> JSONEncodableDictionary { - creator.requestBody(for: operation, - sendOperationIdentifiers: false, + creator.requestBody(for: operation, sendQueryDocument: true, autoPersistQuery: false) } diff --git a/Tests/ApolloTests/TestCustomRequestBodyCreator.swift b/Tests/ApolloTests/TestCustomRequestBodyCreator.swift index 48e349f5b2..1d67767687 100644 --- a/Tests/ApolloTests/TestCustomRequestBodyCreator.swift +++ b/Tests/ApolloTests/TestCustomRequestBodyCreator.swift @@ -15,7 +15,6 @@ struct TestCustomRequestBodyCreator: RequestBodyCreator { func requestBody( for operation: Operation, - sendOperationIdentifiers: Bool, sendQueryDocument: Bool, autoPersistQuery: Bool ) -> JSONEncodableDictionary { stubbedRequestBody diff --git a/Tests/ApolloTests/UploadRequestTests.swift b/Tests/ApolloTests/UploadRequestTests.swift index 043538e807..ccf66d95eb 100644 --- a/Tests/ApolloTests/UploadRequestTests.swift +++ b/Tests/ApolloTests/UploadRequestTests.swift @@ -56,7 +56,7 @@ class UploadRequestTests: XCTestCase { --TEST.BOUNDARY Content-Disposition: form-data; name="operations" -{"id":"c5d5919f77d9ba16a9689b6b0ad4b781cb05dc1dc4812623bf80f7c044c09533","operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) {\\n singleUpload(file: $file) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"file":null}} +{"operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) {\\n singleUpload(file: $file) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"file":null}} --TEST.BOUNDARY Content-Disposition: form-data; name="map" @@ -104,7 +104,7 @@ Alpha file content. --TEST.BOUNDARY Content-Disposition: form-data; name="operations" -{"id":"88858c283bb72f18c0049dc85b140e72a4046f469fa16a8bf4bcf01c11d8a2b7","operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) {\\n multipleUpload(files: $files) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"files":[null,null]}} +{"operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) {\\n multipleUpload(files: $files) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"files":[null,null]}} --TEST.BOUNDARY Content-Disposition: form-data; name="map" @@ -166,7 +166,7 @@ Bravo file content. --TEST.BOUNDARY Content-Disposition: form-data; name="operations" -{"id":"1ec89997a185c50bacc5f62ad41f27f3070f4a950d72e4a1510a4c64160812d5","operationName":"UploadMultipleFilesToDifferentParameters","query":"mutation UploadMultipleFilesToDifferentParameters($singleFile: Upload!, $multipleFiles: [Upload!]!) {\\n multipleParameterUpload(singleFile: $singleFile, multipleFiles: $multipleFiles) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"multipleFiles":["b.txt","c.txt"],\"secondField\":null,"singleFile":"a.txt","uploads\":null}} +{"operationName":"UploadMultipleFilesToDifferentParameters","query":"mutation UploadMultipleFilesToDifferentParameters($singleFile: Upload!, $multipleFiles: [Upload!]!) {\\n multipleParameterUpload(singleFile: $singleFile, multipleFiles: $multipleFiles) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"multipleFiles":["b.txt","c.txt"],\"secondField\":null,"singleFile":"a.txt","uploads\":null}} --TEST.BOUNDARY Content-Disposition: form-data; name="map" From 6c74ead4ff032ddab361de5fb13865a2964ff5fb Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 23 Sep 2021 13:50:00 -0700 Subject: [PATCH 2/5] Refactor GraphQLOperation definition --- Sources/Apollo/HTTPRequest.swift | 8 +- Sources/Apollo/RequestBodyCreator.swift | 9 +- Sources/ApolloAPI/FragmentProtocols.swift | 4 +- Sources/ApolloAPI/GraphQLNullable.swift | 7 ++ Sources/ApolloAPI/GraphQLOperation.swift | 83 +++++++++++++++--- Sources/ApolloTestSupport/MockOperation.swift | 10 +-- .../ApolloWebSocket/WebSocketTransport.swift | 2 +- Sources/UploadAPI/API.swift | 36 ++++---- .../AutomaticPersistedQueriesTests.swift | 14 +-- Tests/ApolloTests/GETTransformerTests.swift | 85 ++++++++++--------- ...electionSetMapper_FromResponse_Tests.swift | 22 ++--- .../ApolloTests/RequestBodyCreatorTests.swift | 2 +- 12 files changed, 178 insertions(+), 104 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index eb11f67c96..0efde6df08 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -83,10 +83,10 @@ extension HTTPRequest: Equatable { public static func == (lhs: HTTPRequest, rhs: HTTPRequest) -> Bool { lhs.graphQLEndpoint == rhs.graphQLEndpoint - && lhs.contextIdentifier == rhs.contextIdentifier - && lhs.additionalHeaders == rhs.additionalHeaders - && lhs.cachePolicy == rhs.cachePolicy - && lhs.operation.queryDocument == rhs.operation.queryDocument + && lhs.contextIdentifier == rhs.contextIdentifier + && lhs.additionalHeaders == rhs.additionalHeaders + && lhs.cachePolicy == rhs.cachePolicy + && lhs.operation.definition?.queryDocument == rhs.operation.definition?.queryDocument } } diff --git a/Sources/Apollo/RequestBodyCreator.swift b/Sources/Apollo/RequestBodyCreator.swift index c5c2cbbab0..0171290fb2 100644 --- a/Sources/Apollo/RequestBodyCreator.swift +++ b/Sources/Apollo/RequestBodyCreator.swift @@ -5,13 +5,13 @@ import ApolloUtils #endif public protocol RequestBodyCreator { - /// Creates a `GraphQLMap` out of the passed-in operation + /// Creates a `JSONEncodableDictionary` out of the passed-in operation /// /// - Parameters: /// - operation: The operation to use /// - sendQueryDocument: Whether or not to send the full query document. Should default to `true`. /// - autoPersistQuery: Whether to use auto-persisted query information. Should default to `false`. - /// - Returns: The created `GraphQLMap` + /// - Returns: The created `JSONEncodableDictionary` func requestBody( for operation: Operation, sendQueryDocument: Bool, @@ -37,7 +37,10 @@ extension RequestBodyCreator { } if sendQueryDocument { - body["query"] = operation.queryDocument + guard let document = operation.definition?.queryDocument else { + preconditionFailure("To send query documents, Apollo types must be generated with `OperationDefinition`s.") + } + body["query"] = document } if autoPersistQuery { diff --git a/Sources/ApolloAPI/FragmentProtocols.swift b/Sources/ApolloAPI/FragmentProtocols.swift index d02a945b78..690843887b 100644 --- a/Sources/ApolloAPI/FragmentProtocols.swift +++ b/Sources/ApolloAPI/FragmentProtocols.swift @@ -6,7 +6,9 @@ /// any `Fragment` included in it's `Fragments` object via its `fragments` property. /// /// - SeeAlso: `HasFragments`, `ToFragments` -public protocol Fragment: AnySelectionSet { } +public protocol Fragment: AnySelectionSet { + var fragmentDefinition: String { get } +} // MARK: - HasFragments diff --git a/Sources/ApolloAPI/GraphQLNullable.swift b/Sources/ApolloAPI/GraphQLNullable.swift index 8cd0b1701b..65f18e30a3 100644 --- a/Sources/ApolloAPI/GraphQLNullable.swift +++ b/Sources/ApolloAPI/GraphQLNullable.swift @@ -1,5 +1,12 @@ import Foundation +/// Indicates the presence of a value, supporting both `nil` and `null` values. +/// +/// In GraphQL, explicitly providing a `null` value for an input value to a field argument is +/// semantically different from not providing a value at all (`nil`). This enum allows you to +/// distinguish your input values between `null` and `nil`. +/// +/// - See: [GraphQLSpec - Input Values - Null Value](http://spec.graphql.org/June2018/#sec-Null-Value) @dynamicMemberLookup public enum GraphQLNullable: ExpressibleByNilLiteral { diff --git a/Sources/ApolloAPI/GraphQLOperation.swift b/Sources/ApolloAPI/GraphQLOperation.swift index 03661b3f8d..fddbfa0433 100644 --- a/Sources/ApolloAPI/GraphQLOperation.swift +++ b/Sources/ApolloAPI/GraphQLOperation.swift @@ -6,16 +6,67 @@ public enum GraphQLOperationType { case subscription } +/// The means of providing the operation document that includes the definition of the operation +/// over network transport. +/// +/// This data represents the `Document` as defined in the GraphQL Spec. +/// - See: [GraphQLSpec - Document](https://spec.graphql.org/draft/#Document) +/// +/// The Apollo Code Generation Engine will generate the `DocumentType` on each generated +/// `GraphQLOperation`. You can change the type of `DocumentType` generated in your +/// [code generation configuration](// TODO: ADD URL TO DOCUMENTATION HERE). +public enum DocumentType { + /// The traditional way of providing the operation `Document`. + /// The `Document` is sent with every operation request. + case notPersisted(definition: OperationDefinition) + + /// Automatically persists your operations using Apollo Server's + /// [APQs](https://www.apollographql.com/docs/apollo-server/performance/apq). + /// + /// This allow the operation definition to be persisted using an `operationIdentifier` instead of + /// being sent with every operation request. If the server does not recognize the + /// `operationIdentifier`, the network transport can send the provided definition to + /// "automatically persist" the operation definition. + case automaticallyPersisted(operationIdentifier: String, definition: OperationDefinition) + + /// Provides only the `operationIdentifier` for operations that have been previously persisted + /// to an Apollo Server using + /// [APQs](https://www.apollographql.com/docs/apollo-server/performance/apq). + /// + /// If the server does not recognize the `operationIdentifier`, the operation will fail. This + /// method should only be used if you are manually persisting your queries to an Apollo Server. + case persistedOperationsOnly(operationIdentifier: String) +} + +/// The definition of an operation to be provided over network transport. +/// +/// This data represents the `Definition` for a `Document` as defined in the GraphQL Spec. +/// In the case of the Apollo client, the definition will always be an `ExecutableDefinition`. +/// - See: [GraphQLSpec - Document](https://spec.graphql.org/draft/#Document) +public struct OperationDefinition { + let operationDefinition: String + let fragments: [Fragment]? + + public init(_ definition: String, fragments: [Fragment]? = nil) { + self.operationDefinition = definition + self.fragments = fragments + } + + public var queryDocument: String { + var document = operationDefinition + fragments?.forEach { + document.append("\n" + $0.fragmentDefinition) + } + return document + } +} + public protocol GraphQLOperation: AnyObject { typealias Variables = [String: GraphQLOperationVariableValue] - var operationType: GraphQLOperationType { get } - - var operationDefinition: String { get } - var operationIdentifier: String? { get } var operationName: String { get } - - var queryDocument: String { get } + var operationType: GraphQLOperationType { get } + var document: DocumentType { get } var variables: Variables? { get } @@ -23,16 +74,24 @@ public protocol GraphQLOperation: AnyObject { } public extension GraphQLOperation { - var queryDocument: String { - return operationDefinition + var variables: Variables? { + return nil } - var operationIdentifier: String? { - return nil + var definition: OperationDefinition? { + switch self.document { + case let .automaticallyPersisted(_, definition), let .notPersisted(definition): + return definition + default: return nil + } } - var variables: Variables? { - return nil + var operationIdentifier: String? { + switch self.document { + case let .automaticallyPersisted(identifier, _), let .persistedOperationsOnly(identifier): + return identifier + default: return nil + } } } diff --git a/Sources/ApolloTestSupport/MockOperation.swift b/Sources/ApolloTestSupport/MockOperation.swift index 791400c475..247f3e4912 100644 --- a/Sources/ApolloTestSupport/MockOperation.swift +++ b/Sources/ApolloTestSupport/MockOperation.swift @@ -5,12 +5,8 @@ open class MockOperation: GraphQLOperation { public let operationType: GraphQLOperationType - public var operationDefinition: String = "None" - public var operationIdentifier: String? public var operationName: String = "MockOperationName" - - public var stubbedQueryDocument: String? - public final var queryDocument: String { stubbedQueryDocument ?? operationDefinition } + public var document: DocumentType = .notPersisted(definition: .init("None")) open var variables: Variables? @@ -97,4 +93,8 @@ open class AbstractMockSelectionSet: AnySelectionSet { open class MockSelectionSet: AbstractMockSelectionSet, RootSelectionSet { } +open class MockFragment: AbstractMockSelectionSet, RootSelectionSet, Fragment { + public var fragmentDefinition: String = "" +} + open class MockTypeCase: AbstractMockSelectionSet, TypeCase { } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 576fc9074a..d89383305b 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -94,7 +94,7 @@ public class WebSocketTransport { /// If false, remember to call `resumeWebSocketConnection()` to connect. /// Defaults to true. public let connectOnInit: Bool - /// [optional]The payload to send on connection. Defaults to an empty `GraphQLMap`. + /// [optional]The payload to send on connection. Defaults to an empty `JSONEncodableDictionary`. public fileprivate(set) var connectingPayload: JSONEncodableDictionary? /// The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. public let requestBodyCreator: RequestBodyCreator diff --git a/Sources/UploadAPI/API.swift b/Sources/UploadAPI/API.swift index 105031ad22..72fbd32c8e 100644 --- a/Sources/UploadAPI/API.swift +++ b/Sources/UploadAPI/API.swift @@ -36,8 +36,10 @@ public final class File: Object { // MARK: - Mutations public final class UploadMultipleFilesToTheSameParameterMutation: GraphQLMutation { - /// The raw GraphQL definition of this operation. - public let operationDefinition: String = + public let operationName: String = "UploadMultipleFilesToTheSameParameter" + public let document: DocumentType = .automaticallyPersisted( + operationIdentifier: "88858c283bb72f18c0049dc85b140e72a4046f469fa16a8bf4bcf01c11d8a2b7", + definition: .init( """ mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) { multipleUpload(files: $files) { @@ -48,11 +50,7 @@ public final class UploadMultipleFilesToTheSameParameterMutation: GraphQLMutatio mimetype } } - """ - - public let operationName: String = "UploadMultipleFilesToTheSameParameter" - - public let operationIdentifier: String? = "88858c283bb72f18c0049dc85b140e72a4046f469fa16a8bf4bcf01c11d8a2b7" + """)) public var files: [String] @@ -100,8 +98,10 @@ public final class UploadMultipleFilesToTheSameParameterMutation: GraphQLMutatio } public final class UploadMultipleFilesToDifferentParametersMutation: GraphQLMutation { - /// The raw GraphQL definition of this operation. - public let operationDefinition: String = + public let operationName: String = "UploadMultipleFilesToDifferentParameters" + public let document: DocumentType = .automaticallyPersisted( + operationIdentifier: "1ec89997a185c50bacc5f62ad41f27f3070f4a950d72e4a1510a4c64160812d5", + definition: .init( """ mutation UploadMultipleFilesToDifferentParameters($singleFile: Upload!, $multipleFiles: [Upload!]!) { multipleParameterUpload(singleFile: $singleFile, multipleFiles: $multipleFiles) { @@ -112,11 +112,7 @@ public final class UploadMultipleFilesToDifferentParametersMutation: GraphQLMuta mimetype } } - """ - - public let operationName: String = "UploadMultipleFilesToDifferentParameters" - - public let operationIdentifier: String? = "1ec89997a185c50bacc5f62ad41f27f3070f4a950d72e4a1510a4c64160812d5" + """)) public var singleFile: String public var multipleFiles: [String] @@ -176,8 +172,10 @@ public final class UploadMultipleFilesToDifferentParametersMutation: GraphQLMuta } public final class UploadOneFileMutation: GraphQLMutation { - /// The raw GraphQL definition of this operation. - public let operationDefinition: String = + public let operationName: String = "UploadOneFile" + public let document: DocumentType = .automaticallyPersisted( + operationIdentifier: "c5d5919f77d9ba16a9689b6b0ad4b781cb05dc1dc4812623bf80f7c044c09533", + definition: .init( """ mutation UploadOneFile($file: Upload!) { singleUpload(file: $file) { @@ -188,11 +186,7 @@ public final class UploadOneFileMutation: GraphQLMutation { mimetype } } - """ - - public let operationName: String = "UploadOneFile" - - public let operationIdentifier: String? = "c5d5919f77d9ba16a9689b6b0ad4b781cb05dc1dc4812623bf80f7c044c09533" + """)) public var file: String diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index 746dd40f64..1ad4cbdbe3 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -43,16 +43,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { self.episode = episode super.init() self.variables = ["episode": episode] - self.operationIdentifier = "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671" - self.operationDefinition = "MockHeroNameQuery - Operation Definition" + self.document = .automaticallyPersisted( + operationIdentifier: "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671", + definition: .init("MockHeroNameQuery - Operation Definition")) } } fileprivate class APQMockMutation: MockMutation { override init() { super.init() - self.operationIdentifier = "4a1250de93ebcb5cad5870acf15001112bf27bb963e8709555b5ff67a1405374" - self.operationDefinition = "APQMockMutation - Operation Definition" + self.document = .automaticallyPersisted( + operationIdentifier: "4a1250de93ebcb5cad5870acf15001112bf27bb963e8709555b5ff67a1405374", + definition: .init("APQMockMutation - Operation Definition")) } } @@ -77,7 +79,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let queryString = jsonBody["query"] as? String if queryDocument { XCTAssertEqual(queryString, - operation.queryDocument, + operation.definition?.queryDocument, file: file, line: line) } @@ -146,7 +148,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let queryString = url.queryItemDictionary?["query"] if queryDocument { XCTAssertEqual(queryString, - query.queryDocument, + query.definition?.queryDocument, file: file, line: line) } else { diff --git a/Tests/ApolloTests/GETTransformerTests.swift b/Tests/ApolloTests/GETTransformerTests.swift index a1ea8d40e4..53d5c07f8c 100644 --- a/Tests/ApolloTests/GETTransformerTests.swift +++ b/Tests/ApolloTests/GETTransformerTests.swift @@ -29,14 +29,16 @@ class GETTransformerTests: XCTestCase { func test__createGetURL__queryWithSingleParameterAndVariable_encodesURL() { let operation = MockOperation.mock() operation.operationName = "TestOpName" - operation.stubbedQueryDocument = """ -query MockQuery($param: String) { - testField(param: $param) { - __typename - name - } -} -""" + operation.document = .notPersisted(definition: .init( + """ + query MockQuery($param: String) { + testField(param: $param) { + __typename + name + } + } + """)) + operation.variables = ["param": "TestParamValue"] let body = requestBodyCreator.requestBody(for: operation, @@ -55,14 +57,15 @@ query MockQuery($param: String) { func test__createGetURL__query_withEnumParameterAndVariable_encodesURL() { let operation = MockOperation.mock() operation.operationName = "TestOpName" - operation.stubbedQueryDocument = """ -query MockQuery($param: MockEnum) { - testField(param: $param) { - __typename - name - } -} -""" + operation.document = .notPersisted(definition: .init( + """ + query MockQuery($param: MockEnum) { + testField(param: $param) { + __typename + name + } + } + """)) operation.variables = ["param": MockEnum.LARGE] let body = requestBodyCreator.requestBody(for: operation, @@ -81,14 +84,16 @@ query MockQuery($param: MockEnum) { func test__createGetURL__queryWithMoreThanOneParameter_withIncludeDirective_encodesURL() throws { let operation = MockOperation.mock() operation.operationName = "TestOpName" - operation.stubbedQueryDocument = """ -query MockQuery($a: String, $b: Boolean!) { - testField(param: $a) { - __typename - nestedField @include(if: $b) - } -} -""" + operation.document = .notPersisted(definition: .init( + """ + query MockQuery($a: String, $b: Boolean!) { + testField(param: $a) { + __typename + nestedField @include(if: $b) + } + } + """)) + operation.variables = ["a": "TestParamValue", "b": true] let body = requestBodyCreator.requestBody(for: operation, @@ -107,8 +112,9 @@ query MockQuery($a: String, $b: Boolean!) { func test__createGetURL__queryWith2DParameter_encodesURL_withBodyComponentsInAlphabeticalOrder() throws { let operation = MockOperation.mock() operation.operationName = "TestOpName" - operation.stubbedQueryDocument = "query MockQuery {}" - operation.operationIdentifier = "4d465fbc6e3731d01102504850" + operation.document = .automaticallyPersisted( + operationIdentifier: "4d465fbc6e3731d01102504850", + definition: .init("query MockQuery {}")) let persistedQuery: JSONEncodableDictionary = [ "version": 1, @@ -120,7 +126,7 @@ query MockQuery($a: String, $b: Boolean!) { ] let body: JSONEncodableDictionary = [ - "query": operation.queryDocument, + "query": operation.definition?.queryDocument, "extensions": extensions ] @@ -141,7 +147,7 @@ query MockQuery($a: String, $b: Boolean!) { ] let body: JSONEncodableDictionary = [ - "query": operation.queryDocument, + "query": operation.definition?.queryDocument, "extensions": extensions ] @@ -162,7 +168,7 @@ query MockQuery($a: String, $b: Boolean!) { ] let body: JSONEncodableDictionary = [ - "query": operation.queryDocument, + "query": operation.definition?.queryDocument, "extensions": extensions ] @@ -177,7 +183,7 @@ query MockQuery($a: String, $b: Boolean!) { func test__createGetURL__queryWithPersistedQueryID_withoutQueryParameter_encodesURL() throws { let operation = MockOperation.mock() operation.operationName = "TestOpName" - operation.operationIdentifier = "4d465fbc6e3731d01102504850" + operation.document = .persistedOperationsOnly(operationIdentifier: "4d465fbc6e3731d01102504850") let persistedQuery: JSONEncodableDictionary = [ "version": 1, @@ -204,14 +210,15 @@ query MockQuery($a: String, $b: Boolean!) { func test__createGetURL__queryWithNullValueForVariable_encodesVariableWithNull() { let operation = MockOperation.mock() operation.operationName = "TestOpName" - operation.stubbedQueryDocument = """ -query MockQuery($param: String) { - testField(param: $param) { - __typename - name - } -} -""" + operation.document = .notPersisted(definition: .init( + """ + query MockQuery($param: String) { + testField(param: $param) { + __typename + name + } + } + """)) operation.variables = ["param": GraphQLNullable.null] let body = requestBodyCreator.requestBody(for: operation, @@ -235,7 +242,7 @@ query MockQuery($param: String) { ] let body: JSONEncodableDictionary = [ - "query": operation.queryDocument, + "query": operation.definition?.queryDocument, "extensions": extensions ] diff --git a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift index 1bfbe799d3..7a4c197715 100644 --- a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift @@ -761,7 +761,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { override class var __typename: String { "MockChildObject" } } - class MockFragment: MockSelectionSet, Fragment { + class GivenFragment: MockFragment { override class var __parentType: ParentType { .Object(MockChildObject.self) } override class var selections: [Selection] {[ .field("child", Child.self) @@ -777,12 +777,12 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { class GivenSelectionSet: MockSelectionSet, HasFragments { override class var __parentType: ParentType { .Object(MockChildObject.self) } override class var selections: [Selection] {[ - .fragment(MockFragment.self) + .fragment(GivenFragment.self) ]} struct Fragments: ResponseObject { let data: ResponseDict - var childFragment: MockFragment { _toFragment() } + var childFragment: GivenFragment { _toFragment() } } } @@ -903,7 +903,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { func test__booleanCondition_include_fragment__givenVariableIsTrue_getsValuesForFragmentFields() throws { // given - class MockFragment: MockSelectionSet, Fragment { + class GivenFragment: MockFragment { override class var selections: [Selection] {[ .field("name", String.self), ]} @@ -912,7 +912,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { class GivenSelectionSet: MockSelectionSet { override class var selections: [Selection] {[ .field("id", String.self), - .include(if: "variable", .fragment(MockFragment.self)) + .include(if: "variable", .fragment(GivenFragment.self)) ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] @@ -928,7 +928,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { func test__booleanCondition_include_fragment__givenVariableIsFalse_doesNotGetValuesForFragmentFields() throws { // given - class MockFragment: MockSelectionSet, Fragment { + class GivenFragment: MockFragment { override class var selections: [Selection] {[ .field("name", String.self), ]} @@ -937,7 +937,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { class GivenSelectionSet: MockSelectionSet { override class var selections: [Selection] {[ .field("id", String.self), - .include(if: "variable", .fragment(MockFragment.self)) + .include(if: "variable", .fragment(GivenFragment.self)) ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] @@ -1109,7 +1109,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { func test__booleanCondition_include_typeCaseOnNamedFragment__givenVariableIsTrue_typeCaseMatchesParentType_getsValuesForTypeCaseFields() throws { // given class Person: Object {} - class MockFragment: MockSelectionSet, Fragment { + class GivenFragment: MockFragment { override class var selections: [Selection] {[ .field("name", String.self), ]} @@ -1124,7 +1124,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { class AsPerson: MockTypeCase { override class var __parentType: ParentType { .Object(Person.self)} override class var selections: [Selection] {[ - .fragment(MockFragment.self), + .fragment(GivenFragment.self), ]} } } @@ -1221,7 +1221,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { func test__booleanCondition_skip_singleField__givenVariableIsTrue_givenFieldIdSelectedByAnotherSelection_getsValueForField() throws { // given - class MockFragment: MockSelectionSet, Fragment { + class GivenFragment: MockFragment { override class var selections: [Selection] {[ .field("name", String.self), ]} @@ -1230,7 +1230,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { class GivenSelectionSet: MockSelectionSet { override class var selections: [Selection] {[ .skip(if: "variable", .field("name", String.self)), - .fragment(MockFragment.self) + .fragment(GivenFragment.self) ]} } let object: JSONObject = ["name": "Luke Skywalker"] diff --git a/Tests/ApolloTests/RequestBodyCreatorTests.swift b/Tests/ApolloTests/RequestBodyCreatorTests.swift index e35623c4f7..dbd4dd58e8 100644 --- a/Tests/ApolloTests/RequestBodyCreatorTests.swift +++ b/Tests/ApolloTests/RequestBodyCreatorTests.swift @@ -30,7 +30,7 @@ class RequestBodyCreatorTests: XCTestCase { let operation = MockOperation.mock() operation.operationName = "Test Operation Name" operation.variables = ["TestVar": 123] - operation.stubbedQueryDocument = "Test Query Document" + operation.document = .notPersisted(definition: .init("Test Query Document")) let creator = ApolloRequestBodyCreator() From 0ceed15a5417df974078f43eedc65d2a3f12a46f Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 23 Sep 2021 15:05:37 -0700 Subject: [PATCH 3/5] Add todo for implementation of persistedOperationsOnly --- Sources/Apollo/JSONRequest.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 24f9b7ed0f..9cf9287357 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -63,6 +63,12 @@ open class JSONRequest: HTTPRequest { switch operation.operationType { case .query: if isPersistedQueryRetry { +#warning(""" +TODO: if using persistedOperationsOnly, we should throw an error. Not sure if that should be +here or somewhere else in the RequestChain? Probably earlier than this when we actually go to +start a retry. +Need to write unit tests for this new behavior. +""") useGetMethod = self.useGETForPersistedQueryRetry sendQueryDocument = true autoPersistQueries = true From ab2a0fd5a57e9d809bd94ab491e0c281967a52b3 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 23 Sep 2021 15:34:06 -0700 Subject: [PATCH 4/5] Change Requests per PR comments --- Sources/ApolloAPI/GraphQLOperation.swift | 8 +++++--- Sources/ApolloTestSupport/MockOperation.swift | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/ApolloAPI/GraphQLOperation.swift b/Sources/ApolloAPI/GraphQLOperation.swift index fddbfa0433..6d043830b1 100644 --- a/Sources/ApolloAPI/GraphQLOperation.swift +++ b/Sources/ApolloAPI/GraphQLOperation.swift @@ -80,15 +80,17 @@ public extension GraphQLOperation { var definition: OperationDefinition? { switch self.document { - case let .automaticallyPersisted(_, definition), let .notPersisted(definition): + case let .automaticallyPersisted(_, definition), + let .notPersisted(definition): return definition default: return nil } } - + var operationIdentifier: String? { switch self.document { - case let .automaticallyPersisted(identifier, _), let .persistedOperationsOnly(identifier): + case let .automaticallyPersisted(identifier, _), + let .persistedOperationsOnly(identifier): return identifier default: return nil } diff --git a/Sources/ApolloTestSupport/MockOperation.swift b/Sources/ApolloTestSupport/MockOperation.swift index 247f3e4912..742f283e3c 100644 --- a/Sources/ApolloTestSupport/MockOperation.swift +++ b/Sources/ApolloTestSupport/MockOperation.swift @@ -6,7 +6,7 @@ open class MockOperation: GraphQLOperation { public let operationType: GraphQLOperationType public var operationName: String = "MockOperationName" - public var document: DocumentType = .notPersisted(definition: .init("None")) + public var document: DocumentType = .notPersisted(definition: .init("Mock Operation Definition")) open var variables: Variables? From b994a847b39254de71f7accfe98ada1535cf9060 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 23 Sep 2021 16:30:16 -0700 Subject: [PATCH 5/5] Added operation generation to the RFC --- CodegenProposal.md | 151 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 14 deletions(-) diff --git a/CodegenProposal.md b/CodegenProposal.md index 855c131d87..8f69c151cd 100644 --- a/CodegenProposal.md +++ b/CodegenProposal.md @@ -203,12 +203,13 @@ enum SkinCovering { } ``` -# Proposal +# Core Concepts In order to fulfill all of the stated goals of this project, the following approach is proposed for the structure of the Codegen: -# `SelectionSet` - A “View” of an Entity +## `SelectionSet` - A “View” of an Entity +--- We will refer to each individual object fetched in a GraphQL response as an “entity”. An entity defines a single type (object, interface, or union) that has fields on it that can be fetched. @@ -231,7 +232,8 @@ Each animal in the list of `allAnimals` is a single entity. Each of those entiti Each generated data object conforms to a `SelectionSet` protocol, which defines some universal behaviors. Type cases, fragments, and root types all conform to this protocol. For reference see [SelectionSet.swift](Sources/ApolloAPI/SelectionSet.swift). -## `SelectionSet` Data is Represented as Structs With Dictionary Storage +### `SelectionSet` Data is Represented as Structs With Dictionary Storage +--- The generated data objects are structs that have a single stored property. The stored property is to another struct named `ResponseDict`, which has a single stored constant property of type `[String: Any]`. @@ -239,7 +241,8 @@ Often times the same data can be represented by different generated types. For e This allows us to store all the fetched data for an entity one time, rather than duplicating data in memory. The structs allow for hyper-performant conversions, as they are stack allocated at compile time and just increment a pointer to the single storage dictionary reference. -## Field Accessors +### Field Accessors +--- Accessors to the fields that a generated object has are implemented as computed properties that access the dictionary storage. @@ -277,7 +280,8 @@ struct Animal: SelectionSet, HasFragments { In this simple example, the `Animal` object has a nested `Height` object. Each conforms to `SelectionSet` and each has a single stored property let data: `ResponseDict`. The `ResponseDict` is a struct that wraps the dictionary storage, and provides custom subscript accessors for casting/transforming the underlying data to the correct types. For more information and implementation details, see: [ResponseDict.swift](Sources/ApolloAPI/ResponseDict.swift) -# GraphQL Execution +## GraphQL Execution +--- GraphQL execution is the process in which the Apollo iOS client converts raw data — either from a network response or the local cache — into a `SelectionSet`. The execution process determines which fields should be “selected”; maps the data for those fields; decodes raw data to the correct types for the fields; validates that all fields have valid data; and returns `SelectionSet` objects that are guaranteed to be valid. @@ -285,9 +289,24 @@ A field that is “selected” is mapped from the raw data onto the `SelectionSe Because `SelectionSet` field access uses unsafe force casting under the hood, it is necessary that a `SelectionSet` is only ever created via the execution process. A `SelectionSet` that is initialized manually cannot be guaranteed to contain all the expected data for its field accessors, and as such, could cause crashes at run time. `SelectionSet`s returned from GraphQL execution are guaranteed to be safe. +## Nullable Arguments - `GraphQLNullable` +--- + +By default, `GraphQLOperation` field variables; fields on `InputObject`s; and field arguments are nullable. For a nullable argument, the value can be provided as a value, a `null` value, or omitted entirely. In `GraphQL`, omitting an argument and passing a `null` value have semantically different meanings. While often, they may be identical, it is up to the implementation of the server to interpret these values. For example, a `null` value for an argument on a mutation may indicate that a field on the object should be set to `null`, while omitting the argument indicates that the field should retain it's current value -- or be set to a default value. + +Because of the semantic difference between `null` and ommitted arguments, we have introduced `GraphQLNullable`. `GraphQLNullable` is a generic enum that acts very similarly to `Optional`, but it differentiates between a `nil` value (the `.none` case), and a `null` value (the `.null` case). Values are still wrapped using the `.some(value)` case as in `Optional`. + +The previous Apollo versions used double optionals (`??`) to represent `null` vs ` nil`. This was unclear to most users and make reading and reasoning about your code difficult in many situations. `GraphQLNullable` makes your intentions clear and explicit when dealing with nullable input values. + +For more information and implementation details, see: [GraphQLNullable.swift](Sources/ApolloAPI/GraphQLNullable.swift) + +# Generated Objects + +An overview of the format of all generated object types. + # Schema Type Generation -In addition to generating `SelectionSet`s for your GraphQL operations, types will be generated for each type (object, interface, or union) that is used in any operations across your entire application. These types will include all the fields that may be fetched by any operation used and can include other type metadata. +In addition to generating `SelectionSet`s for your `GraphQLOperation`s, types will be generated for each type (object, interface, or union) that is used in any operations across your entire application. These types will include all the fields that may be fetched by any operation used and can include other type metadata. The schema types have a number of functions. @@ -319,6 +338,114 @@ A `SchemaConfiguration` object will also be generated for your schema. This obje For an example of generated schema metadata see [AnimalSchema.swift](Sources/ApolloAPI/AnimalSchema.swift). +# `InputObject` Generation + +TODO + +# `EnumType` Generation + +TODO + +# `GraphQLOperation` Generation + +A `GraphQLOperation` is generated for each operation defined in your application. `GraphQLOperation`s can be queries (`GraphQLQuery`), mutations (`GraphQLMutation`), or subscriptions (`GraphQLSubscription`). + +Each generated operation will conform to the `GraphQLOperation` protocol defined in [GraphQLOperation.swift](Sources/ApolloAPI/GraphQLOperation.swift). + +**Simple Operation - Example:** + +```swift +class AnimalQuery: GraphQLQuery { + let operationName: String = "AnimalQuery" + let document: DocumentType = .notPersisted(definition: .init( + """ + query AnimalQuery { + allAnimals { + species + } + } + """) + + init() {} + + struct Data: SelectionSet { + // ... + } +} +``` + +## Operation Arguments + +For an operation that takes input arguments, the initializer will be generated with parameters for each argument. Arguments can be scalar types, `GraphQLEnum`s, or `InputObject`s. During execution, these arguments will be used as the operation's `variables`, which are then used as the values for arguments on `SelectionSet` fields matching the variables name. + +**Operation With Scalar Argument - Example:** + +```swift +class AnimalQuery: GraphQLQuery { + let operationName: String = "AnimalQuery" + let document: DocumentType = .notPersisted(definition: .init( + """ + query AnimalQuery($count: Int!) { + allAnimals { + predators(first: $count) { + species + } + } + } + """) + + var count: Int + + init(count: Int) { + self.count = count + } + + var variables: Variables? { ["count": count] } + + struct Data: SelectionSet { + // ... + struct Animal: SelectionSet { + static var selections: [Selection] {[ + .field("predators", [Predator.self], arguments: ["first": .variable("count")]) + ]} + } + } +} +``` +In this example, the value of the `count` property is passed into the `variables` for the variable with the key `"count"`. The `Selection` for the field `"predators"`, the argument `"first"` has a value of `.variable("count")`. During execution, the `predators` field will be evaluated with the argument from the operation's `"count"` variable. + +### Nullable Operation Arguments + +For nullable arguments, the code generator will wrap the argument value in a `GraphQLNullable`. The executor will evaluate the `GraphQLNullable` to format the operation variables correctly. See [GraphQLNullable](#nullable-arguments-graphqlnullable) for more information. + +**Operation With Nullable Scalar Argument - Example:** + +```swift +class AnimalQuery: GraphQLQuery { + let operationName: String = "AnimalQuery" + let document: DocumentType = .notPersisted(definition: .init( + """ + query AnimalQuery($count: Int) { + allAnimals { + predators(first: $count) { + species + } + } + } + """) + + var count: GraphQLNullable + + init(count: GraphQLNullable) { + self.count = count + } + + var variables: Variables? { ["count": count] } + + // ... +} +``` + # `SelectionSet` Generation ## Metadata @@ -602,7 +729,7 @@ query($count: Int) { ``` ```swift -struct Query: GraphQLQuery { +class Query: GraphQLQuery { var count: Int init(count: Int) { ... } @@ -629,7 +756,7 @@ query($skipSpecies: Boolean) { ``` ```swift -struct Query: GraphQLQuery { +class Query: GraphQLQuery { var skipSpecies: Bool init(skipSpecies: Bool) { ... } @@ -658,7 +785,7 @@ query($includeDetails: Boolean) { ``` ```swift -struct Query: GraphQLQuery { +class Query: GraphQLQuery { var includeDetails: Bool init(includeDetails: Bool) { ... } @@ -715,7 +842,7 @@ fragment AnimalDetails: RootSelectionSet, Fragment { } } -struct Query: GraphQLQuery { +class Query: GraphQLQuery { struct Data: RootSelectionSet { static var selections: [Selection] {[ .field("allAnimals", [Animal].self), @@ -824,10 +951,6 @@ For this reason, only a `RootSelectionSet` can be executed by a `GraphQLExecutor TODO -# `GraphQLEnum` - -TODO - # Cache Key Resolution TODO