Skip to content
Open
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
46 changes: 14 additions & 32 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,6 @@ public RuntimeConfig(
}

SetupDataSourcesUsed();

}

/// <summary>
Expand Down Expand Up @@ -529,12 +528,13 @@ Runtime is not null && Runtime.Host is not null

/// <summary>
/// Returns the ttl-seconds value for a given entity.
/// If the property is not set, returns the global default value set in the runtime config.
/// If the global default value is not set, the default value is used (5 seconds).
/// If the entity explicitly sets ttl-seconds, that value is used.
/// Otherwise, falls back to the global cache TTL setting.
/// Callers are responsible for checking whether caching is enabled before using the result.
/// </summary>
/// <param name="entityName">Name of the entity to check cache configuration.</param>
/// <returns>Number of seconds (ttl) that a cache entry should be valid before cache eviction.</returns>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided or if the entity has caching disabled.</exception>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided.</exception>
public virtual int GetEntityCacheEntryTtl(string entityName)
{
if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
Expand All @@ -545,31 +545,23 @@ public virtual int GetEntityCacheEntryTtl(string entityName)
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

if (!entityConfig.IsCachingEnabled)
{
throw new DataApiBuilderException(
message: $"{entityName} does not have caching enabled.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
}

if (entityConfig.Cache.UserProvidedTtlOptions)
if (entityConfig.Cache is not null && entityConfig.Cache.UserProvidedTtlOptions)
{
return entityConfig.Cache.TtlSeconds.Value;
}
else
{
return GlobalCacheEntryTtl();
}

return GlobalCacheEntryTtl();
}

/// <summary>
/// Returns the cache level value for a given entity.
/// If the property is not set, returns the default (L1L2) for a given entity.
/// If the entity explicitly sets level, that value is used.
/// Otherwise, falls back to the global cache level or the default.
/// Callers are responsible for checking whether caching is enabled before using the result.
/// </summary>
/// <param name="entityName">Name of the entity to check cache configuration.</param>
/// <returns>Cache level that a cache entry should be stored in.</returns>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided or if the entity has caching disabled.</exception>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided.</exception>
public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)
{
if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
Expand All @@ -580,22 +572,12 @@ public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

if (!entityConfig.IsCachingEnabled)
{
throw new DataApiBuilderException(
message: $"{entityName} does not have caching enabled.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
}

if (entityConfig.Cache.UserProvidedLevelOptions)
if (entityConfig.Cache is not null && entityConfig.Cache.UserProvidedLevelOptions)
{
return entityConfig.Cache.Level.Value;
}
else
{
return EntityCacheLevel.L1L2;
}

return EntityCacheOptions.DEFAULT_LEVEL;
}

/// <summary>
Expand Down
44 changes: 44 additions & 0 deletions src/Service.Tests/Caching/CachingConfigProcessingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,4 +416,48 @@ private static string GetRawConfigJson(string globalCacheConfig, string entityCa

return expectedRuntimeConfigJson.ToString();
}

/// <summary>
/// Regression test: Validates that when global runtime cache is enabled but entity cache is disabled,
/// GetEntityCacheEntryTtl and GetEntityCacheEntryLevel do not throw and return sensible defaults.
/// Previously, these methods threw a DataApiBuilderException (BadRequest/NotSupported) when the entity
/// had caching disabled, which caused 400 errors for valid requests when the global cache was enabled.
/// These methods are now pure accessors that always return a value regardless of cache enablement.
/// </summary>
/// <param name="globalCacheConfig">Global cache configuration JSON fragment.</param>
/// <param name="entityCacheConfig">Entity cache configuration JSON fragment.</param>
/// <param name="expectedTtl">Expected TTL returned by GetEntityCacheEntryTtl.</param>
/// <param name="expectedLevel">Expected cache level returned by GetEntityCacheEntryLevel.</param>
[DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 10 }", @",""cache"": { ""enabled"": false }", 10, EntityCacheLevel.L1L2, DisplayName = "Global cache enabled with custom TTL, entity cache disabled: entity returns global TTL and default level.")]
[DataRow(@",""cache"": { ""enabled"": true }", @",""cache"": { ""enabled"": false }", 5, EntityCacheLevel.L1L2, DisplayName = "Global cache enabled with default TTL, entity cache disabled: entity returns default TTL and default level.")]
[DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 10 }", @"", 10, EntityCacheLevel.L1L2, DisplayName = "Global cache enabled with custom TTL, entity cache omitted: entity returns global TTL and default level.")]
[DataTestMethod]
public void GetEntityCacheEntryTtlAndLevel_DoesNotThrow_WhenRuntimeCacheEnabledAndEntityCacheDisabled(
string globalCacheConfig,
string entityCacheConfig,
int expectedTtl,
EntityCacheLevel expectedLevel)
{
// Arrange
string fullConfig = GetRawConfigJson(globalCacheConfig: globalCacheConfig, entityCacheConfig: entityCacheConfig);
RuntimeConfigLoader.TryParseConfig(
json: fullConfig,
out RuntimeConfig? config,
replacementSettings: null);

Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed.");
Assert.IsTrue(config.IsCachingEnabled, message: "Global caching should be enabled for this test scenario.");

Entity entity = config.Entities.First().Value;
Assert.IsFalse(entity.IsCachingEnabled, message: "Entity caching should be disabled for this test scenario.");

string entityName = config.Entities.First().Key;

// Act & Assert - These calls must not throw.
int actualTtl = config.GetEntityCacheEntryTtl(entityName);
EntityCacheLevel actualLevel = config.GetEntityCacheEntryLevel(entityName);

Assert.AreEqual(expected: expectedTtl, actual: actualTtl, message: "GetEntityCacheEntryTtl should return the global/default TTL when entity cache is disabled.");
Assert.AreEqual(expected: expectedLevel, actual: actualLevel, message: "GetEntityCacheEntryLevel should return the default level when entity cache is disabled.");
}
}
5 changes: 3 additions & 2 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2903,7 +2903,7 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission()
}";
string queryName = "stock_by_pk";

ValidateMutationSucceededAtDbLayer(server, client, graphQLQuery, queryName, authToken, AuthorizationResolver.ROLE_AUTHENTICATED);
await ValidateMutationSucceededAtDbLayer(server, client, graphQLQuery, queryName, authToken, AuthorizationResolver.ROLE_AUTHENTICATED);
}
finally
{
Expand Down Expand Up @@ -3225,7 +3225,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous()
/// <param name="query">GraphQL query/mutation text</param>
/// <param name="queryName">GraphQL query/mutation name</param>
/// <param name="authToken">Auth token for the graphQL request</param>
private static async void ValidateMutationSucceededAtDbLayer(TestServer server, HttpClient client, string query, string queryName, string authToken, string clientRoleHeader)
private static async Task ValidateMutationSucceededAtDbLayer(TestServer server, HttpClient client, string query, string queryName, string authToken, string clientRoleHeader)
{
JsonElement queryResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(
client,
Expand All @@ -3237,6 +3237,7 @@ private static async void ValidateMutationSucceededAtDbLayer(TestServer server,
clientRoleHeader: clientRoleHeader);

Assert.IsNotNull(queryResponse);
Assert.AreNotEqual(JsonValueKind.Null, queryResponse.ValueKind, "Expected a JSON object response but received null.");
Assert.IsFalse(queryResponse.TryGetProperty("errors", out _));
}

Expand Down