Skip to content

Commit

Permalink
Fixed collisions of aliases in projections (#3614)
Browse files Browse the repository at this point in the history
  • Loading branch information
PascalSenn authored Apr 28, 2021
1 parent 99fa769 commit ac6d865
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ public Selection RewriteSelection(
type.ContextData.TryGetValue(AlwaysProjectedFieldsKey, out object? fieldsObj) &&
fieldsObj is string[] fields)
{
int aliasCount = 0;
for (var i = 0; i < fields.Length; i++)
{
if (!context.Fields.ContainsKey(fields[i]))
if (!context.Fields.TryGetValue(fields[i], out var field) ||
field.Field.Name != fields[i])
{
IObjectField nodesField = type.Fields[fields[i]];
var alias = "__projection_alias_" + aliasCount++;
var nodesFieldNode = new FieldNode(
null,
new NameNode(fields[i]),
null,
new NameNode(alias),
Array.Empty<DirectiveNode>(),
Array.Empty<ArgumentNode>(),
null);
Expand All @@ -45,7 +48,7 @@ public Selection RewriteSelection(
arguments: selection.Arguments,
internalSelection: true);

context.Fields[fields[i]] = compiledSelection;
context.Fields[alias] = compiledSelection;
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/HotChocolate/Data/test/Data.Tests/Book.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public class Book
{
public int Id { get; set; }

[IsProjected(true)]
public int AuthorId { get; set; }

public string? Title { get; set; }
Expand Down
34 changes: 34 additions & 0 deletions src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,40 @@ fragment Test on Book {
result.ToJson().MatchSnapshot(new SnapshotNameExtension("Result"));
}

[Fact]
public async Task
ExecuteAsync_Should_ProjectAndPage_When_AliasIsSameAsAlwaysProjectedField()
{
// arrange
IRequestExecutor executor = await new ServiceCollection()
.AddGraphQL()
.AddFiltering()
.EnableRelaySupport()
.AddSorting()
.AddProjections()
.AddQueryType(c => c.Name("Query"))
.AddTypeExtension<PagingAndProjectionExtension>()
.AddObjectType<Book>(x =>
x.ImplementsNode().IdField(x => x.Id).ResolveNode(x => default!))
.BuildRequestExecutorAsync();

// act
IExecutionResult result = await executor.ExecuteAsync(
@"
{
books {
nodes {
authorId: title
}
}
}
");

// assert
executor.Schema.Print().MatchSnapshot(new SnapshotNameExtension("Schema"));
result.ToJson().MatchSnapshot(new SnapshotNameExtension("Result"));
}

[Fact]
public async Task CreateSchema_CodeFirst_AsyncQueryable()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": {
"books": {
"nodes": [
{
"authorId": "BookTitle"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
schema {
query: Query
}

"The node interface is implemented by entities that have a global unique identifier."
interface Node {
id: ID!
}

type Author {
id: Int!
name: String
books: [Book!]!
}

type Book implements Node {
id: ID!
authorId: Int!
title: String
author: Author
}

"A connection to a list of items."
type BookConnection {
"Information to aid in pagination."
pageInfo: PageInfo!
"A list of edges."
edges: [BookEdge!]
"A flattened list of the nodes."
nodes: [Book!]
}

"An edge in a connection."
type BookEdge {
"A cursor for use in pagination."
cursor: String!
"The item at the end of the edge."
node: Book!
}

"Information about pagination in a connection."
type PageInfo {
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String
}

type Query {
node(id: ID!): Node
books(first: Int after: String last: Int before: String where: BookFilterInput order: [BookSortInput!]): BookConnection
}

input AuthorFilterInput {
and: [AuthorFilterInput!]
or: [AuthorFilterInput!]
id: ComparableInt32OperationFilterInput
name: StringOperationFilterInput
books: ListFilterInputTypeOfBookFilterInput
}

input AuthorSortInput {
id: SortEnumType
name: SortEnumType
}

input BookFilterInput {
and: [BookFilterInput!]
or: [BookFilterInput!]
id: ComparableInt32OperationFilterInput
authorId: ComparableInt32OperationFilterInput
title: StringOperationFilterInput
author: AuthorFilterInput
}

input BookSortInput {
id: SortEnumType
authorId: SortEnumType
title: SortEnumType
author: AuthorSortInput
}

input ComparableInt32OperationFilterInput {
eq: Int
neq: Int
in: [Int!]
nin: [Int!]
gt: Int
ngt: Int
gte: Int
ngte: Int
lt: Int
nlt: Int
lte: Int
nlte: Int
}

input ListFilterInputTypeOfBookFilterInput {
all: BookFilterInput
none: BookFilterInput
some: BookFilterInput
any: Boolean
}

input StringOperationFilterInput {
and: [StringOperationFilterInput!]
or: [StringOperationFilterInput!]
eq: String
neq: String
contains: String
ncontains: String
in: [String]
nin: [String]
startsWith: String
nstartsWith: String
endsWith: String
nendsWith: String
}

enum SortEnumType {
ASC
DESC
}

"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`."
directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT

"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`."
directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD

0 comments on commit ac6d865

Please sign in to comment.