diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 755a18d53e..1e4f2db7a9 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -124,7 +124,6 @@ 9F69FFA91D42855900E000B1 /* NetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */; }; 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */; }; 9F86B68B1E6438D700B885FF /* GraphQLSelectionSetMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F86B68A1E6438D700B885FF /* GraphQLSelectionSetMapper.swift */; }; - 9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F86B68F1E65533D00B885FF /* GraphQLResponseGenerator.swift */; }; 9F8A958D1EC0FFAB00304A2D /* ApolloInternalTestHelpers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloInternalTestHelpers.framework */; }; 9F8E0BD325668552000D9FA5 /* DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FADC84E1E6B865E00C677E6 /* DataLoader.swift */; }; 9F8E0BE325668559000D9FA5 /* PossiblyDeferred.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F33D6A32566475600A1543F /* PossiblyDeferred.swift */; }; @@ -1260,7 +1259,6 @@ 9F8622F71EC2004200C38162 /* ReadWriteFromStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteFromStoreTests.swift; sourceTree = ""; }; 9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FragmentConstructionAndConversionTests.swift; sourceTree = ""; }; 9F86B68A1E6438D700B885FF /* GraphQLSelectionSetMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLSelectionSetMapper.swift; sourceTree = ""; }; - 9F86B68F1E65533D00B885FF /* GraphQLResponseGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLResponseGenerator.swift; sourceTree = ""; }; 9F8A95781EC0FC1200304A2D /* ApolloInternalTestHelpers.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApolloInternalTestHelpers.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutatingResultsTests.swift; sourceTree = ""; }; 9FA6ABC51EC0A9F7000017BE /* FetchQueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchQueryTests.swift; sourceTree = ""; }; @@ -3750,7 +3748,6 @@ children = ( 9FC2333C1E66BBF7001E4541 /* GraphQLDependencyTracker.swift */, 9F295E371E277B2A00A24949 /* GraphQLResultNormalizer.swift */, - 9F86B68F1E65533D00B885FF /* GraphQLResponseGenerator.swift */, 9F86B68A1E6438D700B885FF /* GraphQLSelectionSetMapper.swift */, ); name = Accumulators; @@ -5420,7 +5417,6 @@ 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */, 9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */, 9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */, - 9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 37952f1de8..a64fb900f7 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -101,9 +101,11 @@ public class ApolloStore { /// - body: The body of the operation to perform. /// - callbackQueue: [optional] The callback queue to use to perform the completion block on. Will perform on the current queue if not provided. Defaults to nil. /// - completion: [optional] The completion block to perform when the read transaction completes. Defaults to nil. - public func withinReadTransaction(_ body: @escaping (ReadTransaction) throws -> T, - callbackQueue: DispatchQueue? = nil, - completion: ((Result) -> Void)? = nil) { + public func withinReadTransaction( + _ body: @escaping (ReadTransaction) throws -> T, + callbackQueue: DispatchQueue? = nil, + completion: ((Result) -> Void)? = nil + ) { self.queue.async { do { let returnValue = try body(ReadTransaction(store: self)) @@ -129,9 +131,11 @@ public class ApolloStore { /// - body: The body of the operation to perform /// - callbackQueue: [optional] a callback queue to perform the action on. Will perform on the current queue if not provided. Defaults to nil. /// - completion: [optional] a completion block to fire when the read-write transaction completes. Defaults to nil. - public func withinReadWriteTransaction(_ body: @escaping (ReadWriteTransaction) throws -> T, - callbackQueue: DispatchQueue? = nil, - completion: ((Result) -> Void)? = nil) { + public func withinReadWriteTransaction( + _ body: @escaping (ReadWriteTransaction) throws -> T, + callbackQueue: DispatchQueue? = nil, + completion: ((Result) -> Void)? = nil + ) { self.queue.async(flags: .barrier) { do { let returnValue = try body(ReadWriteTransaction(store: self)) @@ -156,7 +160,11 @@ public class ApolloStore { /// - Parameters: /// - query: The query to load results for /// - resultHandler: The completion handler to execute on success or error - public func load(_ operation: Operation, callbackQueue: DispatchQueue? = nil, resultHandler: @escaping GraphQLResultHandler) { + public func load( + _ operation: Operation, + callbackQueue: DispatchQueue? = nil, + resultHandler: @escaping GraphQLResultHandler + ) { withinReadTransaction({ transaction in let (data, dependentKeys) = try transaction.readObject( ofType: Operation.Data.self, @@ -185,13 +193,13 @@ public class ApolloStore { fileprivate lazy var loader: DataLoader = DataLoader(self.cache.loadRecords) fileprivate lazy var executor = GraphQLExecutor { object, info in - return object[info.cacheKeyForField] - } resolveReference: { [weak self] reference in - guard let self = self else { - return .immediate(.failure(ApolloStore.Error.notWithinReadTransaction)) - } - return self.loadObject(forKey: reference.key) + return object[info.cacheKeyForField] + } resolveReference: { [weak self] reference in + guard let self = self else { + return .immediate(.failure(ApolloStore.Error.notWithinReadTransaction)) } + return self.loadObject(forKey: reference.key) + } fileprivate init(store: ApolloStore) { self.cache = store.cache @@ -235,7 +243,7 @@ public class ApolloStore { ) } - private final func loadObject(forKey key: CacheKey) -> PossiblyDeferred { + fileprivate final func loadObject(forKey key: CacheKey) -> PossiblyDeferred { self.loader[key].map { record in guard let record = record else { throw JSONDecodingError.missingValue } return record.fields @@ -270,9 +278,16 @@ public class ApolloStore { variables: GraphQLOperation.Variables? = nil, _ body: (inout SelectionSet) throws -> Void ) throws { - var object = try readObject(ofType: type, - withKey: key, - variables: variables) + var object = try readObject( + ofType: type, + withKey: key, + variables: variables, + accumulator: GraphQLSelectionSetMapper( + stripNullValues: false, + allowMissingValuesForOptionalFields: true + ) + ) + try body(&object) try write(selectionSet: object, withKey: key, variables: variables) } diff --git a/Sources/Apollo/GraphQLDependencyTracker.swift b/Sources/Apollo/GraphQLDependencyTracker.swift index 0063f9dc93..f1442b894f 100644 --- a/Sources/Apollo/GraphQLDependencyTracker.swift +++ b/Sources/Apollo/GraphQLDependencyTracker.swift @@ -13,6 +13,10 @@ final class GraphQLDependencyTracker: GraphQLResultAccumulator { dependentKeys.insert(info.cachePath.joined) } + func acceptMissingValue(info: FieldExecutionInfo) throws -> () { + dependentKeys.insert(info.cachePath.joined) + } + func accept(list: [Void], info: FieldExecutionInfo) { dependentKeys.insert(info.cachePath.joined) } diff --git a/Sources/Apollo/GraphQLExecutor.swift b/Sources/Apollo/GraphQLExecutor.swift index 6bd24e7915..372a0e448a 100644 --- a/Sources/Apollo/GraphQLExecutor.swift +++ b/Sources/Apollo/GraphQLExecutor.swift @@ -308,10 +308,7 @@ final class GraphQLExecutor { } return PossiblyDeferred { - guard let value = fieldResolver(object, fieldInfo) else { - throw JSONDecodingError.missingValue - } - return value + fieldResolver(object, fieldInfo) }.flatMap { return self.complete(fields: fieldInfo, withValue: $0, @@ -329,7 +326,7 @@ final class GraphQLExecutor { private func complete( fields fieldInfo: FieldExecutionInfo, - withValue value: JSONValue, + withValue value: JSONValue?, accumulator: Accumulator ) -> PossiblyDeferred { complete(fields: fieldInfo, @@ -343,10 +340,14 @@ final class GraphQLExecutor { /// continues recursively. private func complete( fields fieldInfo: FieldExecutionInfo, - withValue value: JSONValue, + withValue value: JSONValue?, asType returnType: Selection.Field.OutputType, accumulator: Accumulator ) -> PossiblyDeferred { + guard let value else { + return PossiblyDeferred { try accumulator.acceptMissingValue(info: fieldInfo) } + } + if value is NSNull && returnType.isNullable { return PossiblyDeferred { try accumulator.acceptNullValue(info: fieldInfo) } } diff --git a/Sources/Apollo/GraphQLResponseGenerator.swift b/Sources/Apollo/GraphQLResponseGenerator.swift deleted file mode 100644 index 3d0b9a8df4..0000000000 --- a/Sources/Apollo/GraphQLResponseGenerator.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -#if !COCOAPODS -import ApolloAPI -#endif - -final class GraphQLResponseGenerator: GraphQLResultAccumulator { - func accept(scalar: JSONValue, info: FieldExecutionInfo) -> JSONValue { - return scalar - } - - func acceptNullValue(info: FieldExecutionInfo) -> JSONValue { - return NSNull() - } - - func accept(list: [JSONValue], info: FieldExecutionInfo) -> JSONValue { - return list - } - - func accept(childObject: JSONObject, info: FieldExecutionInfo) throws -> JSONValue { - return childObject - } - - func accept(fieldEntry: JSONValue, info: FieldExecutionInfo) -> (key: String, value: JSONValue)? { - return (info.responseKeyForField, fieldEntry) - } - - func accept(fieldEntries: [(key: String, value: JSONValue)], info: ObjectExecutionInfo) -> JSONObject { - return JSONObject(fieldEntries, uniquingKeysWith: { (_, last) in last }) - } - - func finish(rootValue: JSONObject, info: ObjectExecutionInfo) throws -> JSONObject { - return rootValue - } -} diff --git a/Sources/Apollo/GraphQLResultAccumulator.swift b/Sources/Apollo/GraphQLResultAccumulator.swift index daa89ce6d8..7217f070f2 100644 --- a/Sources/Apollo/GraphQLResultAccumulator.swift +++ b/Sources/Apollo/GraphQLResultAccumulator.swift @@ -10,6 +10,7 @@ protocol GraphQLResultAccumulator: AnyObject { func accept(scalar: JSONValue, info: FieldExecutionInfo) throws -> PartialResult func acceptNullValue(info: FieldExecutionInfo) throws -> PartialResult + func acceptMissingValue(info: FieldExecutionInfo) throws -> PartialResult func accept(list: [PartialResult], info: FieldExecutionInfo) throws -> PartialResult func accept(childObject: ObjectResult, info: FieldExecutionInfo) throws -> PartialResult @@ -51,6 +52,11 @@ final class Zip2Accumulator PartialResult { + return (try accumulator1.acceptMissingValue(info: info), + try accumulator2.acceptMissingValue(info: info)) + } + func accept(list: [PartialResult], info: FieldExecutionInfo) throws -> PartialResult { let (list1, list2) = unzip(list) return (try accumulator1.accept(list: list1, info: info), @@ -110,6 +116,12 @@ final class Zip3Accumulator PartialResult { + return (try accumulator1.acceptMissingValue(info: info), + try accumulator2.acceptMissingValue(info: info), + try accumulator3.acceptMissingValue(info: info)) + } + func accept(list: [PartialResult], info: FieldExecutionInfo) throws -> PartialResult { let (list1, list2, list3) = unzip(list) return (try accumulator1.accept(list: list1, info: info), diff --git a/Sources/Apollo/GraphQLResultNormalizer.swift b/Sources/Apollo/GraphQLResultNormalizer.swift index ae7cae88c7..e229c1c084 100644 --- a/Sources/Apollo/GraphQLResultNormalizer.swift +++ b/Sources/Apollo/GraphQLResultNormalizer.swift @@ -6,23 +6,28 @@ import ApolloAPI final class GraphQLResultNormalizer: GraphQLResultAccumulator { private var records: RecordSet = [:] - func accept(scalar: JSONValue, info: FieldExecutionInfo) -> JSONValue { + func accept(scalar: JSONValue, info: FieldExecutionInfo) -> JSONValue? { return scalar } - func acceptNullValue(info: FieldExecutionInfo) -> JSONValue { + func acceptNullValue(info: FieldExecutionInfo) -> JSONValue? { return NSNull() } - func accept(list: [JSONValue], info: FieldExecutionInfo) -> JSONValue { + func acceptMissingValue(info: FieldExecutionInfo) -> JSONValue? { + return nil + } + + func accept(list: [JSONValue?], info: FieldExecutionInfo) -> JSONValue? { return list } - func accept(childObject: CacheReference, info: FieldExecutionInfo) throws -> JSONValue { + func accept(childObject: CacheReference, info: FieldExecutionInfo) -> JSONValue? { return childObject } - func accept(fieldEntry: JSONValue, info: FieldExecutionInfo) -> (key: String, value: JSONValue)? { + func accept(fieldEntry: JSONValue?, info: FieldExecutionInfo) -> (key: String, value: JSONValue)? { + guard let fieldEntry else { return nil } return (info.cacheKeyForField, fieldEntry) } diff --git a/Sources/Apollo/GraphQLSelectionSetMapper.swift b/Sources/Apollo/GraphQLSelectionSetMapper.swift index 04e6325a60..c423d0c155 100644 --- a/Sources/Apollo/GraphQLSelectionSetMapper.swift +++ b/Sources/Apollo/GraphQLSelectionSetMapper.swift @@ -2,8 +2,22 @@ import ApolloAPI #endif +import Foundation + /// An accumulator that converts executed data to the correct values to create a `SelectionSet`. final class GraphQLSelectionSetMapper: GraphQLResultAccumulator { + + let stripNullValues: Bool + let allowMissingValuesForOptionalFields: Bool + + init( + stripNullValues: Bool = true, + allowMissingValuesForOptionalFields: Bool = false + ) { + self.stripNullValues = stripNullValues + self.allowMissingValuesForOptionalFields = allowMissingValuesForOptionalFields + } + func accept(scalar: JSONValue, info: FieldExecutionInfo) throws -> JSONValue? { switch info.field.type.namedType { case let .scalar(decodable as any JSONDecodable.Type), @@ -17,6 +31,13 @@ final class GraphQLSelectionSetMapper: GraphQLRes } func acceptNullValue(info: FieldExecutionInfo) -> JSONValue? { + return stripNullValues ? nil : NSNull() + } + + func acceptMissingValue(info: FieldExecutionInfo) throws -> JSONValue? { + guard allowMissingValuesForOptionalFields && info.field.type.isNullable else { + throw JSONDecodingError.missingValue + } return nil } diff --git a/Sources/Apollo/PossiblyDeferred.swift b/Sources/Apollo/PossiblyDeferred.swift index 7585360e09..b23f915500 100644 --- a/Sources/Apollo/PossiblyDeferred.swift +++ b/Sources/Apollo/PossiblyDeferred.swift @@ -69,7 +69,7 @@ enum PossiblyDeferred { } } - /// Returns a new possibly deferred result, mapping any success value using the given + /// Returns a new possibly deferred result, mapping any success value using the given /// transformation and unwrapping the produced result. /// /// Use this method to avoid a nested result when your transformation diff --git a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift index 691b889b3b..c94a453113 100644 --- a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift +++ b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift @@ -433,6 +433,207 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } + func test_updateCacheMutationWithOptionalField_containingNull_updateNestedField_updatesObjectsMaintainingNullValue() throws { + // given + struct GivenSelectionSet: MockMutableRootSelectionSet { + public var __data: DataDict = DataDict([:], variables: nil) + init(data: DataDict) { __data = data } + + static var __selections: [Selection] { [ + .field("hero", Hero.self) + ]} + + var hero: Hero { + get { __data["hero"] } + set { __data["hero"] = newValue } + } + + struct Hero: MockMutableRootSelectionSet { + public var __data: DataDict = DataDict([:], variables: nil) + init(data: DataDict) { __data = data } + + static var __selections: [Selection] { [ + .field("name", String.self), + .field("nickname", String?.self) + ]} + + var name: String { + get { __data["name"] } + set { __data["name"] = newValue } + } + + var nickname: String? { + get { __data["nickname"] } + set { __data["nickname"] = newValue } + } + } + } + + let cacheMutation = MockLocalCacheMutation() + + mergeRecordsIntoCache([ + "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], + "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "nickname": NSNull()] + ]) + + runActivity("update mutation") { _ in + let updateCompletedExpectation = expectation(description: "Update completed") + + store.withinReadWriteTransaction({ transaction in + try transaction.update(cacheMutation) { data in + data.hero.name = "Artoo" + } + }, completion: { result in + defer { updateCompletedExpectation.fulfill() } + XCTAssertSuccessResult(result) + }) + + self.wait(for: [updateCompletedExpectation], timeout: Self.defaultWaitTimeout) + } + + let query = MockQuery() + + loadFromStore(operation: query) { result in + try XCTAssertSuccessResult(result) { graphQLResult in + XCTAssertEqual(graphQLResult.source, .cache) + XCTAssertNil(graphQLResult.errors) + + let data = try XCTUnwrap(graphQLResult.data) + XCTAssertEqual(data.hero.name, "Artoo") + XCTAssertNil(data.hero.nickname) + } + } + } + + /// The 'nickname' field is currently a cache miss, as it has never been fetched. We want to be able + /// to successfully mutate the 'name' field, but the 'nickname' field should still be a cache miss. + /// While reading an optional field to execute a cache mutation, this is fine, but while reading the + /// omitted optional field to execute a fetch from the cache onto a immutable selection set for a + /// operation, this should throw a missing value error, indicating the cache miss. + func test_updateCacheMutationWithOptionalField_omittingOptionalField_updateNestedField_updatesObjectsMaintainingNilValue_throwsMissingValueErrorOnRead() throws { + // given + struct GivenSelectionSet: MockMutableRootSelectionSet { + public var __data: DataDict = DataDict([:], variables: nil) + init(data: DataDict) { __data = data } + + static var __selections: [Selection] { [ + .field("hero", Hero.self) + ]} + + var hero: Hero { + get { __data["hero"] } + set { __data["hero"] = newValue } + } + + struct Hero: MockMutableRootSelectionSet { + public var __data: DataDict = DataDict([:], variables: nil) + init(data: DataDict) { __data = data } + + static var __selections: [Selection] { [ + .field("name", String.self), + .field("nickname", String?.self) + ]} + + var name: String { + get { __data["name"] } + set { __data["name"] = newValue } + } + + var nickname: String? { + get { __data["nickname"] } + set { __data["nickname"] = newValue } + } + } + } + + let cacheMutation = MockLocalCacheMutation() + + mergeRecordsIntoCache([ + "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], + "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2"] + ]) + + runActivity("update mutation") { _ in + let updateCompletedExpectation = expectation(description: "Update completed") + + store.withinReadWriteTransaction({ transaction in + try transaction.update(cacheMutation) { data in + data.hero.name = "Artoo" + } + }, completion: { result in + defer { updateCompletedExpectation.fulfill() } + XCTAssertSuccessResult(result) + }) + + self.wait(for: [updateCompletedExpectation], timeout: Self.defaultWaitTimeout) + } + + let query = MockQuery() + + loadFromStore(operation: query) { result in + expectJSONMissingValueError(result, atPath: ["hero", "nickname"]) + } + } + + func test_updateCacheMutationWithNonNullField_withNilValue_updateNestedField_throwsMissingValueOnInitialReadForUpdate() throws { + // given + struct GivenSelectionSet: MockMutableRootSelectionSet { + public var __data: DataDict = DataDict([:], variables: nil) + init(data: DataDict) { __data = data } + + static var __selections: [Selection] { [ + .field("hero", Hero.self) + ]} + + var hero: Hero { + get { __data["hero"] } + set { __data["hero"] = newValue } + } + + struct Hero: MockMutableRootSelectionSet { + public var __data: DataDict = DataDict([:], variables: nil) + init(data: DataDict) { __data = data } + + static var __selections: [Selection] { [ + .field("name", String.self), + .field("nickname", String.self) + ]} + + var name: String { + get { __data["name"] } + set { __data["name"] = newValue } + } + + var nickname: String { + get { __data["nickname"] } + set { __data["nickname"] = newValue } + } + } + } + + let cacheMutation = MockLocalCacheMutation() + + mergeRecordsIntoCache([ + "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], + "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2"] + ]) + + runActivity("update mutation") { _ in + let updateCompletedExpectation = expectation(description: "Update completed") + + store.withinReadWriteTransaction({ transaction in + try transaction.update(cacheMutation) { data in + data.hero.name = "Artoo" + } + }, completion: { result in + defer { updateCompletedExpectation.fulfill() } + expectJSONMissingValueError(result, atPath: ["hero", "nickname"]) + }) + + self.wait(for: [updateCompletedExpectation], timeout: Self.defaultWaitTimeout) + } + } + func test_updateCacheMutation_givenMutationOperation_updateNestedField_updatesObjectAtMutationRoot() throws { // given struct GivenSelectionSet: MockMutableRootSelectionSet { @@ -1169,7 +1370,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { .field("hero", Hero.self) ]} - var hero: Hero? { + var hero: Hero { get { __data["hero"] } set { __data["hero"] = newValue } } @@ -1179,7 +1380,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { init(data: DataDict) { __data = data } static var __selections: [Selection] { [ - .field("name", String.self) + .field("name", String?.self) ]} var name: String? { @@ -1193,7 +1394,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let writeCompletedExpectation = expectation(description: "Write completed") store.withinReadWriteTransaction({ transaction in - let data = GivenSelectionSet(data: DataDict([:], variables: nil)) + let data = GivenSelectionSet(data: DataDict(["hero": "name"], variables: nil)) let cacheMutation = MockLocalCacheMutation() try transaction.write(data: data, for: cacheMutation) }, completion: { result in @@ -1202,7 +1403,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { XCTAssertFailureResult(result) { error in if let error = error as? GraphQLExecutionError { XCTAssertEqual(error.path, ["hero"]) - XCTAssertMatch(error.underlying, JSONDecodingError.missingValue) + XCTAssertMatch(error.underlying, JSONDecodingError.wrongType) } else { XCTFail("Unexpected error: \(error)") } @@ -1835,8 +2036,8 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { // MARK: Helpers -fileprivate func expectJSONMissingValueError( - _ result: Result<(), Error>, +fileprivate func expectJSONMissingValueError( + _ result: Result, atPath path: ResponsePath, file: FileString = #file, line: UInt = #line ) { diff --git a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift index 1343f65264..d515559bd8 100644 --- a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift @@ -26,7 +26,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { selectionSet: selectionSet, on: object, variables: variables, - accumulator: GraphQLSelectionSetMapper() + accumulator: GraphQLSelectionSetMapper(stripNullValues: true) ) }