Skip to content

Commit 973d932

Browse files
calvincestarigh-action-runner
authored andcommitted
fix: Cache read null list item (#637)
1 parent 5f59a3f commit 973d932

File tree

2 files changed

+193
-4
lines changed

2 files changed

+193
-4
lines changed

Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,65 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading {
341341
}
342342
}
343343
}
344-
344+
345+
func testLoadingHeroAndFriendsNamesQuery_withOptionalFriendsSelection_withNullFriendListItem() throws {
346+
// given
347+
class GivenSelectionSet: MockSelectionSet {
348+
override class var __selections: [Selection] { [
349+
.field("hero", Hero.self)
350+
]}
351+
var hero: Hero { __data["hero"] }
352+
353+
class Hero: MockSelectionSet {
354+
override class var __selections: [Selection] {[
355+
.field("__typename", String.self),
356+
.field("name", String.self),
357+
.field("friends", [Friend?]?.self)
358+
]}
359+
var friends: [Friend?]? { __data["friends"] }
360+
361+
class Friend: MockSelectionSet {
362+
override class var __selections: [Selection] {[
363+
.field("__typename", String.self),
364+
.field("name", String.self)
365+
]}
366+
var name: String { __data["name"] }
367+
}
368+
}
369+
}
370+
371+
mergeRecordsIntoCache([
372+
"QUERY_ROOT": ["hero": CacheReference("hero")],
373+
"hero": [
374+
"name": "R2-D2",
375+
"__typename": "Droid",
376+
"friends": [
377+
CacheReference("hero.friends.0"),
378+
NSNull(),
379+
]
380+
],
381+
"hero.friends.0": ["__typename": "Human", "name": "Luke Skywalker"],
382+
])
383+
384+
// when
385+
let query = MockQuery<GivenSelectionSet>()
386+
387+
loadFromStore(operation: query) { result in
388+
// then
389+
try XCTAssertSuccessResult(result) { graphQLResult in
390+
XCTAssertEqual(graphQLResult.source, .cache)
391+
XCTAssertNil(graphQLResult.errors)
392+
393+
let data = try XCTUnwrap(graphQLResult.data)
394+
XCTAssertEqual(data.hero.name, "R2-D2")
395+
396+
XCTAssertEqual(data.hero.friends?.count, 2)
397+
XCTAssertEqual(data.hero.friends![0]!.name, "Luke Skywalker")
398+
XCTAssertNil(data.hero.friends![1]) // Null friend at position 2
399+
}
400+
}
401+
}
402+
345403
func testLoadingHeroAndFriendsNamesQuery_withOptionalFriendsSelection_withFriendsNotInCache_throwsMissingValueError() throws {
346404
// given
347405
class GivenSelectionSet: MockSelectionSet {
@@ -494,4 +552,131 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading {
494552
}
495553
}
496554
}
555+
556+
@MainActor func testLoadingHeroAndFriendsNamesQuery_withOptionalFriendsSelection_withNullFriendListItem_usingRequestChain() throws {
557+
// given
558+
struct Types {
559+
static let Hero = Object(typename: "Hero", implementedInterfaces: [])
560+
static let Friend = Object(typename: "Friend", implementedInterfaces: [])
561+
}
562+
563+
MockSchemaMetadata.stub_objectTypeForTypeName({
564+
switch $0 {
565+
case "Hero":
566+
return Types.Hero
567+
case "Friend":
568+
return Types.Friend
569+
default:
570+
XCTFail()
571+
return nil
572+
}
573+
})
574+
575+
class Hero: MockSelectionSet {
576+
typealias Schema = MockSchemaMetadata
577+
578+
override class var __parentType: any ParentType { Types.Hero }
579+
override class var __selections: [Selection] {[
580+
.field("__typename", String.self),
581+
.field("name", String.self),
582+
.field("friends", [Friend?]?.self)
583+
]}
584+
585+
public var name: String? { __data["name"] }
586+
public var friends: [Friend?]? { __data["friends"] }
587+
588+
convenience init(
589+
name: String? = nil,
590+
friends: [Friend?]? = nil
591+
) {
592+
self.init(_dataDict: DataDict(
593+
data: [
594+
"__typename": Types.Hero.typename,
595+
"name": name,
596+
"friends": friends
597+
],
598+
fulfilledFragments: [ObjectIdentifier(Self.self)]
599+
))
600+
}
601+
602+
class Friend: MockSelectionSet {
603+
override class var __selections: [Selection] {[
604+
.field("__typename", String.self),
605+
.field("name", String.self)
606+
]}
607+
var name: String { __data["name"] }
608+
}
609+
}
610+
611+
// given
612+
let client = MockURLSessionClient(
613+
response: .mock(
614+
url: TestURL.mockServer.url,
615+
statusCode: 200,
616+
httpVersion: nil,
617+
headerFields: nil
618+
),
619+
data: """
620+
{
621+
"data": {
622+
"__typename": "Hero",
623+
"name": "R2-D2",
624+
"friends": [
625+
{
626+
"__typename": "Friend",
627+
"name": "Luke Skywalker"
628+
},
629+
null,
630+
{
631+
"__typename": "Friend",
632+
"name": "Obi-Wan Kenobi"
633+
}
634+
]
635+
}
636+
}
637+
""".data(using: .utf8)
638+
)
639+
640+
let requestChain: (any RequestChain)? = InterceptorRequestChain(interceptors: [
641+
NetworkFetchInterceptor(client: client),
642+
JSONResponseParsingInterceptor(),
643+
CacheWriteInterceptor(store: self.store),
644+
])
645+
646+
let request = JSONRequest(
647+
operation: MockQuery<Hero>(),
648+
graphQLEndpoint: TestURL.mockServer.url,
649+
clientName: "test-client",
650+
clientVersion: "test-client-version"
651+
)
652+
653+
let expectation = expectation(description: "Response received")
654+
655+
// when
656+
requestChain?.kickoff(request: request) { result in
657+
defer {
658+
expectation.fulfill()
659+
}
660+
661+
XCTAssertSuccessResult(result)
662+
}
663+
664+
wait(for: [expectation], timeout: 2)
665+
666+
loadFromStore(operation: MockQuery<Hero>()) { result in
667+
// then
668+
try XCTAssertSuccessResult(result) { graphQLResult in
669+
XCTAssertEqual(graphQLResult.source, .cache)
670+
XCTAssertNil(graphQLResult.errors)
671+
672+
let data = try XCTUnwrap(graphQLResult.data)
673+
XCTAssertEqual(data.name, "R2-D2")
674+
675+
XCTAssertEqual(data.friends?.count, 3)
676+
XCTAssertEqual(data.friends![0]!.name, "Luke Skywalker")
677+
XCTAssertNil(data.friends![1]) // Null friend at position 2
678+
XCTAssertEqual(data.friends![2]!.name, "Obi-Wan Kenobi")
679+
}
680+
}
681+
}
497682
}

apollo-ios/Sources/Apollo/ExecutionSources/CacheDataExecutionSource.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
4141
case let reference as CacheReference:
4242
return deferredResolve(reference: reference).map { $0 as AnyHashable }
4343

44-
case let referenceList as [CacheReference]:
44+
case let referenceList as [JSONValue]:
4545
return referenceList
4646
.enumerated()
4747
.deferredFlatMap { index, element in
48-
self.deferredResolve(reference: element)
48+
guard let cacheReference = element as? CacheReference else {
49+
return .immediate(.success(element))
50+
}
51+
52+
return self.deferredResolve(reference: cacheReference)
4953
.mapError { error in
5054
if !(error is GraphQLExecutionError) {
5155
return GraphQLExecutionError(
@@ -55,7 +59,7 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
5559
} else {
5660
return error
5761
}
58-
}
62+
}.map { $0 as AnyHashable }
5963
}.map { $0._asAnyHashable }
6064

6165
default:

0 commit comments

Comments
 (0)