Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix (defer): deferred fragment condition evaluation #353

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase {
)
}

private func readValues<T: SelectionSet, Operation: GraphQLOperation>(
_ selectionSet: T.Type,
in operation: Operation.Type,
from object: JSONObject,
variables: GraphQLOperation.Variables? = nil
) throws -> T {
return try GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.executor.execute(
selectionSet: selectionSet,
in: operation,
on: object,
accumulator: GraphQLSelectionSetMapper<T>()
)
}

// MARK: - Tests

// MARK: Nonnull Scalar
Expand Down Expand Up @@ -952,7 +966,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase {

// MARK: Deferred Inline Fragments

func test__deferredInlineFragment__whenExecutingOnDeferredInlineFragment_selectsFieldsAndFulfillsFragment() throws {
func test__deferredInlineFragment__givenPartialDataForSelection_withConditionEvaluatingTrue_collectsDeferredFragment() throws {
// given
class AnAnimal: MockSelectionSet {
typealias Schema = MockSchemaMetadata
Expand All @@ -967,7 +981,174 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase {
override class var __selections: [Selection] {[
.field("__typename", String.self),
.field("name", String.self),
.deferred(DeferredSpecies.self, label: "deferreSpecies"),
.deferred(if: "varA", DeferredSpecies.self, label: "deferredSpecies"),
]}

struct Fragments: FragmentContainer {
public let __data: DataDict
public init(_dataDict: DataDict) {
__data = _dataDict
_deferredSpecies = Deferred(_dataDict: _dataDict)
}

@Deferred var deferredSpecies: DeferredSpecies?
}

class DeferredSpecies: MockTypeCase {
override class var __selections: [Selection] {[
.field("species", String.self),
]}
}
}
}

let object: JSONObject = [
"animal": [
"__typename": "Animal",
"name": "Lassie"
]
]

// when
let data = try readValues(AnAnimal.self, from: object, variables: ["varA": true])

// then
XCTAssertEqual(data.animal.__typename, "Animal")
XCTAssertEqual(data.animal.name, "Lassie")

XCTAssertEqual(data.animal.__data._deferredFragments, [ObjectIdentifier(AnAnimal.Animal.DeferredSpecies.self)])
XCTAssertEqual(data.animal.__data._fulfilledFragments, [ObjectIdentifier(AnAnimal.Animal.self)])
}

func test__deferredInlineFragment__givenPartialDataForSelection_withConditionEvaluatingFalse_doesCollectFulfilledFragmentAndFields() throws {
// given
class AnAnimal: MockSelectionSet {
typealias Schema = MockSchemaMetadata

override class var __selections: [Selection] {[
.field("animal", Animal.self),
]}

var animal: Animal { __data["animal"] }

class Animal: AbstractMockSelectionSet<Animal.Fragments, MockSchemaMetadata> {
override class var __selections: [Selection] {[
.field("__typename", String.self),
.field("name", String.self),
.deferred(if: "varA", DeferredSpecies.self, label: "deferredSpecies"),
]}

struct Fragments: FragmentContainer {
public let __data: DataDict
public init(_dataDict: DataDict) {
__data = _dataDict
_deferredSpecies = Deferred(_dataDict: _dataDict)
}

@Deferred var deferredSpecies: DeferredSpecies?
}

class DeferredSpecies: MockTypeCase {
override class var __selections: [Selection] {[
.field("species", String.self),
]}
}
}
}

let object: JSONObject = [
"animal": [
"__typename": "Animal",
"name": "Lassie",
"species": "Canis familiaris",
]
]

// when
let data = try readValues(AnAnimal.self, from: object, variables: ["varA": false])

// then
XCTAssertEqual(data.animal.__typename, "Animal")
XCTAssertEqual(data.animal.name, "Lassie")
XCTAssertEqual(data.animal.fragments.deferredSpecies?.species, "Canis familiaris")

XCTAssertTrue(data.animal.__data._deferredFragments.isEmpty)
XCTAssertEqual(data.animal.__data._fulfilledFragments, [
ObjectIdentifier(AnAnimal.Animal.self),
ObjectIdentifier(AnAnimal.Animal.DeferredSpecies.self)
])
}

func test__deferredInlineFragment__givenPartialDataForSelection_withConditionEvaluatingFalse_whenMissingDeferredIncrementalData_shouldThrow() throws {
// given
class AnAnimal: MockSelectionSet {
typealias Schema = MockSchemaMetadata

override class var __selections: [Selection] {[
.field("animal", Animal.self),
]}

var animal: Animal { __data["animal"] }

class Animal: AbstractMockSelectionSet<Animal.Fragments, MockSchemaMetadata> {
override class var __selections: [Selection] {[
.field("__typename", String.self),
.field("name", String.self),
.deferred(if: "varA", DeferredSpecies.self, label: "deferredSpecies"),
]}

struct Fragments: FragmentContainer {
public let __data: DataDict
public init(_dataDict: DataDict) {
__data = _dataDict
_deferredSpecies = Deferred(_dataDict: _dataDict)
}

@Deferred var deferredSpecies: DeferredSpecies?
}

class DeferredSpecies: MockTypeCase {
override class var __selections: [Selection] {[
.field("species", String.self),
]}
}
}
}

let object: JSONObject = [
"animal": [
"__typename": "Animal",
"name": "Lassie"
]
]

// when + then
XCTAssertThrowsError(try readValues(AnAnimal.self, from: object, variables: ["varA": false])) { error in
guard
let error = error as? GraphQLExecutionError,
case JSONDecodingError.missingValue = error.underlying
else { return fail("Incorrect error type") }

XCTAssertEqual(error.path, ResponsePath("animal.species"))
}
}

func test__deferredInlineFragment__givenIncrementalDataForDeferredSelection_selectsFieldsAndFulfillsFragment() throws {
// given
class AnAnimal: MockSelectionSet {
typealias Schema = MockSchemaMetadata

override class var __selections: [Selection] {[
.field("animal", Animal.self),
]}

var animal: Animal { __data["animal"] }

class Animal: AbstractMockSelectionSet<Animal.Fragments, MockSchemaMetadata> {
override class var __selections: [Selection] {[
.field("__typename", String.self),
.field("name", String.self),
.deferred(DeferredSpecies.self, label: "deferredSpecies"),
]}

struct Fragments: FragmentContainer {
Expand All @@ -993,17 +1174,13 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase {
]

// when
let data = try GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.executor.execute(
selectionSet: AnAnimal.Animal.DeferredSpecies.self,
in: MockQuery<AnAnimal>.self,
on: object,
accumulator: GraphQLSelectionSetMapper<AnAnimal.Animal.DeferredSpecies>()
)
let data = try readValues(AnAnimal.Animal.DeferredSpecies.self, in: MockQuery<AnAnimal>.self, from: object)

// then
XCTAssertEqual(data.species, "Canis familiaris")

XCTAssertEqual(data.__data._fulfilledFragments, [ObjectIdentifier(AnAnimal.Animal.DeferredSpecies.self)])
XCTAssertTrue(data.__data._deferredFragments.isEmpty)
}

// MARK: - Fragments
Expand Down
27 changes: 21 additions & 6 deletions apollo-ios/Sources/Apollo/FieldSelectionCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,30 @@ struct DefaultFieldSelectionCollector: FieldSelectionCollector {
info: info)
}

case let .deferred(_, typeCase, _):
case let .deferred(condition, typeCase, _):
// In Apollo's implementation (Router + Server) of deferSpec=20220824 ALL defer directives
// will be honoured and sent as separate incremental responses. This means deferred
// selection fields never need to be collected because they are parsed with the incremental
// data, at which time they are no longer deferred.
// selection fields only need to be collected when they are parsed with the incremental
// data, at which time they are no longer deferred. The deferred fragment identifiers still
// need to be collected becuase that is how the user determines the state of the deferred
// fragment via the @Deferred property wrapper.
//
// The deferred fragment identifiers still need to be collected becuase that is how the
// user determines the state of the deferred fragment via the @Deferred property wrapper.
groupedFields.addDeferredFragment(typeCase)
// If the defer condition evaluates to false though, the fragment is considered to be fulfilled
// and and the fields must be collected.
let isDeferred: Bool = {
if let condition, !condition.evaluate(with: info.variables) {
return false
}
return true
}()

if isDeferred {
groupedFields.addDeferredFragment(typeCase)

} else {
groupedFields.addFulfilledFragment(typeCase)
try collectFields(from: typeCase.__selections, into: &groupedFields, for: object, info: info)
}

case let .fragment(fragment):
groupedFields.addFulfilledFragment(fragment)
Expand Down
2 changes: 1 addition & 1 deletion apollo-ios/Sources/ApolloAPI/Selection+Conditions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ fileprivate extension Array where Element == Selection.Condition {
}

// MARK: Conditions - Individual
fileprivate extension Selection.Condition {
public extension Selection.Condition {
func evaluate(with variables: GraphQLOperation.Variables?) -> Bool {
switch self {
case let .value(value):
Expand Down
Loading