Skip to content

Commit

Permalink
fix: SelectionSetTemplate scope comparison (#460)
Browse files Browse the repository at this point in the history
  • Loading branch information
calvincestari authored and gh-action-runner committed Aug 15, 2024
1 parent 8deb7bb commit c0c80cf
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -10545,6 +10545,208 @@ class SelectionSetTemplateTests: XCTestCase {
expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true))
}

func test__render_nestedSelectionSet__givenEntityFieldMerged_fromTypeCase_withInclusionCondition_rendersSelectionSetAsTypeAlias_withFullyQualifiedName() async throws {
// given
schemaSDL = """
type Query {
allAuthors: [Author!]
}
type Author {
name: String
postsInfoByIds: [PostInfo!]
}
interface PostInfo {
awardings: [AwardingTotal!]
}
type AwardingTotal {
id: String!
comments: [Comment!]
total: Int!
}
type Comment {
id: String!
}
type Post implements PostInfo {
id: String!
awardings: [AwardingTotal!]
}
"""

document = """
query TestOperation($a: Boolean = false) {
allAuthors {
name
postsInfoByIds {
... on Post {
awardings {
total
}
}
awardings @include(if: $a) {
comments {
id
}
}
}
}
}
"""

let expectedType = """
public var comments: [Comment]? { __data["comments"] }
/// AllAuthor.PostsInfoById.Awarding.Comment
public struct Comment: TestSchema.SelectionSet {
"""

let expectedTypeAlias = """
public var comments: [Comment]? { __data["comments"] }
public var total: Int { __data["total"] }
public typealias Comment = PostsInfoById.Awarding.Comment
"""

// when
try await buildSubjectAndOperation()
let allAuthors_postsInfoByIds = try XCTUnwrap(
operation[field: "query"]?[field: "allAuthors"]?[field: "postsInfoByIds"]?.selectionSet
)
let allAuthors_postsInfoByIds_awardings = try XCTUnwrap(
allAuthors_postsInfoByIds[field: "awardings"]?.selectionSet
)
let allAuthors_postsInfoByIds_asPost_awardings_ifA = try XCTUnwrap(
allAuthors_postsInfoByIds[as: "Post"]?[field: "awardings"]?[if: "a"]
)

let actualType = subject.test_render(
inlineFragment: allAuthors_postsInfoByIds_awardings.computed
)
let actualTypeAlias = subject.test_render(
inlineFragment: allAuthors_postsInfoByIds_asPost_awardings_ifA.computed
)

// then
expect(actualType).to(equalLineByLine(expectedType, atLine: 12, ignoringExtraLines: true))
expect(actualTypeAlias).to(equalLineByLine(expectedTypeAlias, atLine: 13, ignoringExtraLines: true))
}

func test__render_nestedSelectionSet__givenEntityFieldMerged_fromTypeCase_withInclusionCondition_siblingTypeCaseSameFieldSameCondition_rendersSelectionSetAsTypeAlias_withFullyQualifiedName() async throws {
// given
schemaSDL = """
type Query {
allAuthors: [Thing!]
}
type Thing {
name: String
postsInfoByIds: [PostInfo!]
}
interface PostInfo {
awardings: [AwardingTotal!]
}
type AwardingTotal {
id: String!
comments: [Comment!]
total: Int!
name: String!
}
type Comment {
id: String!
}
type Post implements PostInfo {
id: String!
awardings: [AwardingTotal!]
}
"""

document = """
query TestOperation($a: Boolean = false) {
allAuthors {
name
postsInfoByIds {
... on Post {
awardings {
total
}
... on PostInfo {
awardings @include(if: $a) {
name
}
}
}
awardings @include(if: $a) {
comments {
id
}
}
}
}
}
"""

let expectedType = """
public var comments: [Comment]? { __data["comments"] }
/// AllAuthor.PostsInfoById.Awarding.Comment
public struct Comment: TestSchema.SelectionSet {
public let __data: DataDict
public init(_dataDict: DataDict) { __data = _dataDict }
public static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Comment }
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("id", String.self),
] }
public var id: String { __data["id"] }
}
"""

let expectedTypeAlias = """
public static var __selections: [ApolloAPI.Selection] { [
.field("name", String.self),
] }
public var name: String { __data["name"] }
public var comments: [Comment]? { __data["comments"] }
public var total: Int { __data["total"] }
public typealias Comment = PostsInfoById.Awarding.Comment
"""

// when
try await buildSubjectAndOperation()
let allAuthors_postsInfoByIds = try XCTUnwrap(
operation[field: "query"]?[field: "allAuthors"]?[field: "postsInfoByIds"]?.selectionSet
)
let allAuthors_postsInfoByIds_awardings = try XCTUnwrap(
allAuthors_postsInfoByIds[field: "awardings"]?.selectionSet
)
let allAuthors_postsInfoByIds_asPost_awardings_ifA = try XCTUnwrap(
allAuthors_postsInfoByIds[as: "Post"]?[field: "awardings"]?[if: "a"]
)

let actualType = subject.test_render(
inlineFragment: allAuthors_postsInfoByIds_awardings.computed
)
let actualTypeAlias = subject.test_render(
inlineFragment: allAuthors_postsInfoByIds_asPost_awardings_ifA.computed
)

// then
expect(actualType).to(equalLineByLine(expectedType, atLine: 12, ignoringExtraLines: true))
expect(actualTypeAlias).to(equalLineByLine(expectedTypeAlias, atLine: 8, ignoringExtraLines: true))
}

func test__render_nestedSelectionSet__givenEntityFieldMergedFromParent_notOperationRoot_doesNotRendersTypeAliasForSelectionSet() async throws {
// given
schemaSDL = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,10 @@ extension IR.MergedSelections.MergedSource {
var sourceTypePathCurrentNode = typeInfo.scopePath.last
var nodesToSharedRoot = 0

while targetTypePathCurrentNode.value == sourceTypePathCurrentNode.value {
while representsSameScope(
target: targetTypePathCurrentNode.value,
source: sourceTypePathCurrentNode.value
) {
guard let previousFieldNode = targetTypePathCurrentNode.previous,
let previousSourceNode = sourceTypePathCurrentNode.previous
else {
Expand Down Expand Up @@ -895,6 +898,37 @@ extension IR.MergedSelections.MergedSource {
return selectionSetName
}

/// Checks whether the target and source scope descriptors represent the same scope.
///
/// There is the obvious comparison when the two scope descriptors are equal but there
/// is also a more nuanced edge case that must be considered too.
///
/// This edge case occurs when the target merged source has an inclusion condition that
/// gets broken out into the next node due to the same field existing without an inclusion
/// condition at the target scope. In this case the comparison considers two contiguous
/// nodes with a type condition and an inclusion condition at the root of the entity to
/// match a single node with a matching type condition and inclusion condition.
///
/// See the test named `test__render_nestedSelectionSet__givenEntityFieldMerged_fromTypeCase_withInclusionCondition_rendersSelectionSetAsTypeAlias_withFullyQualifiedName`
/// for a specific test related to this behaviour.
fileprivate func representsSameScope(target: ScopeDescriptor, source: ScopeDescriptor) -> Bool {
guard target != source else { return true }

if target.scopePath.head.value.type == source.scopePath.head.value.type {
guard
let sourceConditions = source.scopePath.head.value.conditions,
target.scopePath[1].type == nil,
let targetNextNodeConditions = target.scopePath[1].conditions
else {
return false
}

return sourceConditions == targetNextNodeConditions
}

return false
}

private func generatedSelectionSetNameForMergedEntity(
in fragment: IR.NamedFragment,
pluralizer: Pluralizer
Expand Down

0 comments on commit c0c80cf

Please sign in to comment.