diff --git a/Sources/ApolloAPI/LocalCacheMutation.swift b/Sources/ApolloAPI/LocalCacheMutation.swift index 2934e5b607..3752e827e3 100644 --- a/Sources/ApolloAPI/LocalCacheMutation.swift +++ b/Sources/ApolloAPI/LocalCacheMutation.swift @@ -18,17 +18,23 @@ public extension LocalCacheMutation { } } -public protocol MutableSelectionSet: AnySelectionSet {} - -public protocol MutableRootSelectionSet: RootSelectionSet, MutableSelectionSet { +public protocol MutableSelectionSet: SelectionSet { var data: DataDict { get set } } -extension MutableRootSelectionSet { - - @inlinable public var __typename: String { +public extension MutableSelectionSet { + @inlinable var __typename: String { get { data["__typename"] } set { data["__typename"] = newValue } } +} +public extension MutableSelectionSet where Fragments: FragmentContainer { + @inlinable var fragments: Fragments { + get { Self.Fragments(data: data) } + set { data._data = newValue.data._data} + } } + +public protocol MutableRootSelectionSet: RootSelectionSet, MutableSelectionSet {} + diff --git a/Sources/ApolloAPI/SelectionSet.swift b/Sources/ApolloAPI/SelectionSet.swift index 947b55311d..f45969328f 100644 --- a/Sources/ApolloAPI/SelectionSet.swift +++ b/Sources/ApolloAPI/SelectionSet.swift @@ -60,9 +60,9 @@ public protocol SelectionSet: AnySelectionSet, Hashable { extension SelectionSet { - public static var schema: SchemaConfiguration.Type { Schema.self } + @inlinable public static var schema: SchemaConfiguration.Type { Schema.self } - var __objectType: Object.Type? { Schema.objectType(forTypename: __typename) } + @usableFromInline var __objectType: Object.Type? { Schema.objectType(forTypename: __typename) } @inlinable public var __typename: String { data["__typename"] } @@ -72,7 +72,7 @@ extension SelectionSet { /// - Warning: This function is not supported for use outside of generated call sites. /// Generated call sites are guaranteed by the GraphQL compiler to be safe. /// Unsupported usage may result in unintended consequences including crashes. - public func _asInlineFragment( + @inlinable public func _asInlineFragment( if conditions: Selection.Conditions? = nil ) -> T? where T.Schema == Schema { guard let conditions = conditions else { @@ -82,30 +82,30 @@ extension SelectionSet { return conditions.evaluate(with: data._variables) ? _asType() : nil } - private func _asType() -> T? where T.Schema == Schema { + @usableFromInline func _asType() -> T? where T.Schema == Schema { guard let __objectType = __objectType, __objectType._canBeConverted(to: T.__parentType) else { return nil } return T.init(data: data) } - public func _asInlineFragment( + @inlinable public func _asInlineFragment( if conditions: [Selection.Condition] ) -> T? where T.Schema == Schema { _asInlineFragment(if: Selection.Conditions([conditions])) } - public func _asInlineFragment( + @inlinable public func _asInlineFragment( if condition: Selection.Condition ) -> T? where T.Schema == Schema { _asInlineFragment(if: Selection.Conditions(condition)) } - public func hash(into hasher: inout Hasher) { + @inlinable public func hash(into hasher: inout Hasher) { hasher.combine(data) } - public static func ==(lhs: Self, rhs: Self) -> Bool { + @inlinable public static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.data == rhs.data } } diff --git a/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift b/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift index bacceffd38..675ee1384b 100644 --- a/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift +++ b/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift @@ -22,9 +22,12 @@ open class MockLocalCacheMutationFromSubscription() + + runActivity("update mutation") { _ in + let updateCompletedExpectation = expectation(description: "Update completed") + + store.withinReadWriteTransaction({ transaction in + try transaction.update(cacheMutation) { data in + data.hero.asDroid?.primaryFunction = "Combat" + } + }, 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.asDroid?.primaryFunction, "Combat") + } + } + } + + func test_updateCacheMutation_updateNestedFieldOnNamedFragment_updatesObjects() throws { + // given + class Droid: Object {} + MockSchemaConfiguration.stub_objectTypeForTypeName = { typename in + switch typename { + case "Droid": return Droid.self + default: return nil + } + } + + struct GivenFragment: MockMutableRootSelectionSet, Fragment { + typealias Schema = MockSchemaConfiguration + static let fragmentDefinition: StaticString = "" + + var data: DataDict = DataDict([:], variables: nil) + + static var selections: [Selection] { [ + .field("__typename", String.self), + .field("name", String.self), + .inlineFragment(AsDroid.self), + ]} + + var name: String { + get { data["name"] } + set { data["name"] = newValue } + } + + var asDroid: AsDroid? { + get { _asInlineFragment() } + set { if let newData = newValue?.data._data { data._data = newData }} + } + + struct AsDroid: MockMutableInlineFragment { + public var data: DataDict = DataDict([:], variables: nil) + static let __parentType: ParentType = .Object(Droid.self) + + static var selections: [Selection] { [ + .field("primaryFunction", String.self), + ]} + + var primaryFunction: String { + get { data["primaryFunction"] } + set { data["primaryFunction"] = newValue } + } + } + } + + struct GivenSelectionSet: MockMutableRootSelectionSet { + public var data: DataDict = DataDict([:], variables: nil) + + 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) + + static var selections: [Selection] { [ + .field("__typename", String.self), + .field("name", String.self), + .fragment(GivenFragment.self), + ]} + + var name: String { + get { data["name"] } + set { data["name"] = newValue } + } + + struct Fragments: FragmentContainer { + var data: DataDict + init(data: DataDict) { self.data = data } + + var givenFragment: GivenFragment { + get { _toFragment() } + set { data._data = newValue.data._data } + } + } + } + } + + mergeRecordsIntoCache([ + "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], + "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "primaryFunction": "Protocol"] + ]) + + let cacheMutation = MockLocalCacheMutation() + + runActivity("update mutation") { _ in + let updateCompletedExpectation = expectation(description: "Update completed") + + store.withinReadWriteTransaction({ transaction in + try transaction.update(cacheMutation) { data in + data.hero.fragments.givenFragment.name = "Artoo" + data.hero.fragments.givenFragment.asDroid?.primaryFunction = "Combat" + } + }, 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") + XCTAssertEqual(data.hero.fragments.givenFragment.asDroid?.primaryFunction, "Combat") + } + } + } + + func test_updateCacheMutation_updateNestedFieldOnOptionalNamedFragment_updatesObjects() throws { + // given + class Droid: Object {} + MockSchemaConfiguration.stub_objectTypeForTypeName = { typename in + switch typename { + case "Droid": return Droid.self + default: return nil + } + } + + struct GivenFragment: MockMutableRootSelectionSet, Fragment { + typealias Schema = MockSchemaConfiguration + static let fragmentDefinition: StaticString = "" + + var data: DataDict = DataDict([:], variables: nil) + + static var selections: [Selection] { [ + .field("__typename", String.self), + .field("name", String.self), + .inlineFragment(AsDroid.self), + ]} + + var name: String { + get { data["name"] } + set { data["name"] = newValue } + } + + var asDroid: AsDroid? { + get { _asInlineFragment() } + set { if let newData = newValue?.data._data { data._data = newData }} + } + + struct AsDroid: MockMutableInlineFragment { + public var data: DataDict = DataDict([:], variables: nil) + static let __parentType: ParentType = .Object(Droid.self) + + static var selections: [Selection] { [ + .field("primaryFunction", String.self), + ]} + + var primaryFunction: String { + get { data["primaryFunction"] } + set { data["primaryFunction"] = newValue } + } + } + } + + struct GivenSelectionSet: MockMutableRootSelectionSet { + public var data: DataDict = DataDict([:], variables: nil) + + 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) + + static var selections: [Selection] { [ + .field("__typename", String.self), + .field("name", String.self), + .fragment(GivenFragment.self), + ]} + + var name: String { + get { data["name"] } + set { data["name"] = newValue } + } + + struct Fragments: FragmentContainer { + var data: DataDict + init(data: DataDict) { self.data = data } + + var givenFragment: GivenFragment? { + get { _toFragment() } + set { if let newData = newValue?.data._data { data._data = newData } } + } + } + } + } + + mergeRecordsIntoCache([ + "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], + "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "primaryFunction": "Protocol"] + ]) + + let cacheMutation = MockLocalCacheMutation() + + runActivity("update mutation") { _ in + let updateCompletedExpectation = expectation(description: "Update completed") + + store.withinReadWriteTransaction({ transaction in + try transaction.update(cacheMutation) { data in + data.hero.fragments.givenFragment?.name = "Artoo" + data.hero.fragments.givenFragment?.asDroid?.primaryFunction = "Combat" + } + }, 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") + XCTAssertEqual(data.hero.fragments.givenFragment?.asDroid?.primaryFunction, "Combat") + } + } + } + func test_updateCacheMutation_givenAddNewReferencedEntity_entityIsIncludedOnRead() throws { /// given struct GivenSelectionSet: MockMutableRootSelectionSet {