Skip to content

Commit

Permalink
Use CacheControlScope.Public per default (#7058)
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias-tengler authored Apr 22, 2024
1 parent 48ffb4a commit 2bab78e
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 25 deletions.
4 changes: 2 additions & 2 deletions src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ protected internal override void TryConfigure(
/// <summary>
/// The maximum time, in seconds, this resource can be cached.
/// </summary>
public int MaxAge { get => _maxAge ?? 0; set => _maxAge = value; }
public int MaxAge { get => _maxAge ?? CacheControlDefaults.MaxAge; set => _maxAge = value; }

/// <summary>
/// The scope of this resource.
/// </summary>
public CacheControlScope Scope
{
get => _scope ?? CacheControlScope.Public;
get => _scope ?? CacheControlDefaults.Scope;
set => _scope = value;
}

Expand Down
7 changes: 7 additions & 0 deletions src/HotChocolate/Caching/src/Caching/CacheControlDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace HotChocolate.Caching;

internal static class CacheControlDefaults
{
public const int MaxAge = 0;
public const CacheControlScope Scope = CacheControlScope.Public;
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private void TryApplyDefaults(RegisteredType type, ObjectTypeDefinition objectDe
{
// Each field on the query type or data resolver fields
// are treated as fields that need to be explicitly cached.
ApplyCacheControlWithDefaultMaxAge(field);
ApplyCacheControlWithDefaults(field);
appliedDefaults = true;
}
}
Expand All @@ -105,16 +105,28 @@ private void TryApplyDefaults(RegisteredType type, ObjectTypeDefinition objectDe
}
}

private void ApplyCacheControlWithDefaultMaxAge(
private void ApplyCacheControlWithDefaults(
OutputFieldDefinitionBase field)
{
var isNotDefaultScope = _cacheControlOptions.DefaultScope != CacheControlDefaults.Scope;

var arguments = new ArgumentNode[isNotDefaultScope ? 2 : 1];
arguments[0] = new ArgumentNode(
CacheControlDirectiveType.Names.MaxAgeArgName,
_cacheControlOptions.DefaultMaxAge);

if (isNotDefaultScope)
{
arguments[1] = new ArgumentNode(
CacheControlDirectiveType.Names.ScopeArgName,
new EnumValueNode(_cacheControlOptions.DefaultScope));
}

field.Directives.Add(
new DirectiveDefinition(
new DirectiveNode(
CacheControlDirectiveType.Names.DirectiveName,
new ArgumentNode(
CacheControlDirectiveType.Names.MaxAgeArgName,
_cacheControlOptions.DefaultMaxAge))));
arguments)));
}

private static bool HasCacheControlDirective(ObjectFieldDefinition field)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ public sealed class CacheControlOptions : ICacheControlOptions
public bool Enable { get; set; } = true;

/// <inheritdoc />
public int DefaultMaxAge { get; set; } = 0;
public int DefaultMaxAge { get; set; } = CacheControlDefaults.MaxAge;

/// <inheritdoc />
public CacheControlScope DefaultScope { get; set; } = CacheControlDefaults.Scope;

/// <inheritdoc />
public bool ApplyDefaults { get; set; } = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,17 @@ public interface ICacheControlOptions
int DefaultMaxAge { get; }

/// <summary>
/// Denotes whether the <see cref="DefaultMaxAge"/> should be applied to all
/// fields that do not already specify a <see cref="CacheControlDirective"/>,
/// are fields on the Query root type or are responsible for fetching data.
/// The <c>Scope</c> that should be applied to fields,
/// if <see cref="ApplyDefaults"/> is <c>true</c>.
/// Defaults to <c>Public</c>.
/// </summary>
CacheControlScope DefaultScope { get; }

/// <summary>
/// Denotes whether the <see cref="DefaultMaxAge"/> and <see cref="DefaultScope"/>
/// should be applied to all fields that do not already specify a
/// <see cref="CacheControlDirective"/>, are fields on the Query root type
/// or are responsible for fetching data.
/// </summary>
bool ApplyDefaults { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ type Query {
.MatchSnapshotAsync();
}

[Fact]
public async Task QueryFields_ApplyDefaults_DifferentDefaultScope()
{
await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(@"
type Query {
field1: String
field2: String @cacheControl(maxAge: 200, scope: PUBLIC)
}
")
.UseField(_ => _)
.AddCacheControl()
.ModifyCacheControlOptions(o => o.DefaultScope = CacheControlScope.Private)
.BuildSchemaAsync()
.MatchSnapshotAsync();
}

[Fact]
public async Task QueryFields_ApplyDefaults_False()
{
Expand Down Expand Up @@ -107,6 +125,18 @@ public async Task DataResolvers_ApplyDefaults_DifferentDefaultMaxAge()
.MatchSnapshotAsync();
}

[Fact]
public async Task DataResolvers_ApplyDefaults_DifferentDefaultScope()
{
await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>()
.AddCacheControl()
.ModifyCacheControlOptions(o => o.DefaultScope = CacheControlScope.Private)
.BuildSchemaAsync()
.MatchSnapshotAsync();
}

[Fact]
public async Task DataResolvers_ApplyDefaults_False()
{
Expand Down
83 changes: 83 additions & 0 deletions src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,89 @@ public async Task MaxAge_Zero_Should_Cache()
result.MatchSnapshot();
}

[Fact]
public async Task Just_Defaults_Should_Cache()
{
var server = CreateServer(services =>
{
services.AddGraphQLServer()
.UseQueryCachePipeline()
.AddCacheControl()
.AddQueryType(d =>
d.Name("Query")
.Field("field")
.Resolve(""));
});

var client = server.CreateClient();
var result = await client.PostQueryAsync("{ field }");

result.MatchSnapshot();
}

[Fact]
public async Task No_Applied_Defaults_Should_Not_Cache()
{
var server = CreateServer(services =>
{
services.AddGraphQLServer()
.UseQueryCachePipeline()
.AddCacheControl()
.ModifyCacheControlOptions(o => o.ApplyDefaults = false)
.AddQueryType(d =>
d.Name("Query")
.Field("field")
.Resolve(""));
});

var client = server.CreateClient();
var result = await client.PostQueryAsync("{ field }");

result.MatchSnapshot();
}

[Fact]
public async Task Default_Max_Age_Should_Apply_And_Cache()
{
var server = CreateServer(services =>
{
services.AddGraphQLServer()
.UseQueryCachePipeline()
.AddCacheControl()
.ModifyCacheControlOptions(o => o.DefaultMaxAge = 1000)
.AddQueryType(d =>
d.Name("Query")
.Field("field")
.Resolve(""));
});

var client = server.CreateClient();
var result = await client.PostQueryAsync("{ field }");

result.MatchSnapshot();
}

[Fact]
public async Task Default_Scope_Should_Apply_And_Cache()
{
var server = CreateServer(services =>
{
services.AddGraphQLServer()
.UseQueryCachePipeline()
.AddCacheControl()
.ModifyCacheControlOptions(o => o.DefaultScope = CacheControlScope.Private)
.AddQueryType(d =>
d.Name("Query")
.Field("field")
.Resolve(""));
});

var client = server.CreateClient();
var result = await client.PostQueryAsync("{ field }");

result.MatchSnapshot();
}

[Fact]
public async Task JustScope_Should_Not_Cache()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
schema {
query: Query
}

"Information about the offset pagination."
type CollectionSegmentInfo {
"Indicates whether more items exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more items exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
}

type NestedType {
taskField: String! @cacheControl(maxAge: 0, scope: PRIVATE)
valueTaskField: String! @cacheControl(maxAge: 0, scope: PRIVATE)
executableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE)
queryableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE)
queryableFieldWithConnection("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionConnection @cacheControl(maxAge: 0, scope: PRIVATE)
queryableFieldWithCollectionSegment(skip: Int take: Int): QueryableFieldWithCollectionSegmentCollectionSegment @cacheControl(maxAge: 0, scope: PRIVATE)
taskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
pureField: String!
pureFieldWithCacheControl: String! @cacheControl(maxAge: 200)
}

type NestedType2 {
taskFieldWithInheritMaxAge: String! @cacheControl(inheritMaxAge: true)
}

"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 {
taskField: String! @cacheControl(maxAge: 0, scope: PRIVATE)
valueTaskField: String! @cacheControl(maxAge: 0, scope: PRIVATE)
executableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE)
queryableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE)
queryableFieldWithConnection("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionConnection @cacheControl(maxAge: 0, scope: PRIVATE)
queryableFieldWithCollectionSegment(skip: Int take: Int): QueryableFieldWithCollectionSegmentCollectionSegment @cacheControl(maxAge: 0, scope: PRIVATE)
taskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
nested: NestedType! @cacheControl(maxAge: 0, scope: PRIVATE)
nested2: NestedType2! @cacheControl(maxAge: 0, scope: PRIVATE)
pureField: String! @cacheControl(maxAge: 0, scope: PRIVATE)
pureFieldWithCacheControl: String! @cacheControl(maxAge: 200)
}

"A segment of a collection."
type QueryableFieldWithCollectionSegmentCollectionSegment {
"Information to aid in pagination."
pageInfo: CollectionSegmentInfo!
"A flattened list of the items."
items: [String!]
}

"A segment of a collection."
type QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment {
"Information to aid in pagination."
pageInfo: CollectionSegmentInfo!
"A flattened list of the items."
items: [String!]
}

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

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

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

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

"The scope of a cache hint."
enum CacheControlScope {
"The value to cache is not tied to a single user."
PUBLIC
"The value to cache is specific to a single user."
PRIVATE
}

"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,3 @@
"extensions": {}
}

{
"message": "Can not specify `inheritMaxAge: true` and a value for `maxAge` for the @cacheControl directive on the field Query.field.",
"type": "Query",
"extensions": {}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
schema {
query: Query
}

type Query {
field1: String @cacheControl(maxAge: 0, scope: PRIVATE)
field2: String @cacheControl(maxAge: 200, scope: PUBLIC)
}

"The scope of a cache hint."
enum CacheControlScope {
"The value to cache is not tied to a single user."
PUBLIC
"The value to cache is specific to a single user."
PRIVATE
}

"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
Loading

0 comments on commit 2bab78e

Please sign in to comment.