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
2 changes: 2 additions & 0 deletions src/Cli.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public static void Init()
VerifierSettings.IgnoreMember<Entity>(entity => entity.EntityFirst);
// Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint.
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsLinkingEntity);
// Ignore the entity IsAutoentity as that's unimportant from a test standpoint.
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsAutoentity);
// Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedTtlOptions);
// Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
Expand Down
7 changes: 6 additions & 1 deletion src/Config/ObjectModel/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public record Entity
[JsonIgnore]
public bool IsLinkingEntity { get; init; }

[JsonIgnore]
public bool IsAutoentity { get; init; }

[JsonConstructor]
public Entity(
EntitySource Source,
Expand All @@ -58,7 +61,8 @@ public Entity(
bool IsLinkingEntity = false,
EntityHealthCheckConfig? Health = null,
string? Description = null,
EntityMcpOptions? Mcp = null)
EntityMcpOptions? Mcp = null,
bool IsAutoentity = false)
{
this.Health = Health;
this.Source = Source;
Expand All @@ -72,6 +76,7 @@ public Entity(
this.IsLinkingEntity = IsLinkingEntity;
this.Description = Description;
this.Mcp = Mcp;
this.IsAutoentity = IsAutoentity;
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, str
return false;
}

public bool RemoveGeneratedAutoentityNameFromDataSourceName(string entityName)
{
return _entityNameToDataSourceName.Remove(entityName);
}

/// <summary>
/// Constructor for runtimeConfig.
/// To be used when setting up from cli json scenario.
Expand Down
19 changes: 19 additions & 0 deletions src/Core/Configurations/RuntimeConfigProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,23 @@ public void AddMergedEntitiesToConfig(Dictionary<string, Entity> newEntities)
};
_configLoader.EditRuntimeConfig(newRuntimeConfig);
}

public void RemoveGeneratedAutoentitiesFromConfig()
{
Dictionary<string, Entity> entities = new(_configLoader.RuntimeConfig!.Entities);
foreach ((string name, Entity entity) in entities)
{
if (entity.IsAutoentity)
{
entities.Remove(name);
_configLoader.RuntimeConfig!.RemoveGeneratedAutoentityNameFromDataSourceName(name);
}
}

Comment on lines +433 to +441
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not throw any error when we remove the autoentities from the collection

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Copilot has a valid point. we should not modify the same collection in the loop. the suggestion from Copilot is a fail-safe approach. collect the keys to remove first and remove them separately.

RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with
{
Entities = new(entities)
};
_configLoader.EditRuntimeConfig(newRuntimeConfig);
}
}
10 changes: 10 additions & 0 deletions src/Core/Configurations/RuntimeConfigValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,16 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig)
foreach ((string entityName, Entity entity) in runtimeConfig.Entities)
{
HashSet<EntityActionOperation> totalSupportedOperationsFromAllRoles = new();

if (entity.Permissions.Length == 0)
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Entity: {entityName} has no permissions defined.",
statusCode: HttpStatusCode.ServiceUnavailable,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we let DAB to be entirely unavailble for a single entity having no permissions or let it pass for others and log in the missing permission entities as warnings/errors and have DAB available?

subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));

}

foreach (EntityPermission permissionSetting in entity.Permissions)
{
string roleName = permissionSetting.Role;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,8 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
Health: autoentity.Template.Health,
Fields: null,
Relationships: null,
Mappings: new());
Mappings: new(),
IsAutoentity: true);

// Add the generated entity to the linking entities dictionary.
// This allows the entity to be processed later during metadata population.
Expand Down
15 changes: 15 additions & 0 deletions src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,12 @@ public async Task InitializeAsync()

GenerateRestPathToEntityMap();
InitODataParser();

if (_isValidateOnly)
{
RemoveGeneratedAutoentities();
}

timer.Stop();
_logger.LogTrace($"Done inferring Sql database schema in {timer.ElapsedMilliseconds}ms.");
}
Expand Down Expand Up @@ -714,6 +720,15 @@ protected virtual Task GenerateAutoentitiesIntoEntities(IReadOnlyDictionary<stri
throw new NotSupportedException($"{GetType().Name} does not support Autoentities yet.");
}

/// <summary>
/// Removes the entities that were generated from the autoentities property.
/// This should only be done when we only want to validate the entities.
/// </summary>
private void RemoveGeneratedAutoentities()
{
_runtimeConfigProvider.RemoveGeneratedAutoentitiesFromConfig();
}

protected void PopulateDatabaseObjectForEntity(
Entity entity,
string entityName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private static void GenerateConfigFile(
string entityBackingColumn = "title",
string entityExposedName = "title",
string mcpEnabled = "true",
string autoentityName = "autoentity_{object}",
string configFileName = CONFIG_FILE_NAME)
{
File.WriteAllText(configFileName, @"
Expand Down Expand Up @@ -180,6 +181,30 @@ private static void GenerateConfigFile(
}
]
}
},
""autoentities"": {
""BooksAutoentities"": {
""patterns"": {
""include"": [ ""%book%"" ],
""name"": """ + autoentityName + @"""
},
""template"": {
""rest"": {
""enabled"": true
}
}
},
""permissions"": [
{
""role"": ""anonymous"",
""actions"": [
{
""action"": ""*""
}
]
}
]
}
}
}");
}
Expand Down Expand Up @@ -768,6 +793,41 @@ await WaitForConditionAsync(
Assert.AreEqual(HttpStatusCode.OK, restResult.StatusCode);
}

/// <summary>
/// Hot reload the configuration file so that it changes the name of the autoentity properties.
/// Then we assert that the hot reload is successful by sending a request to the newly created autoentity.
/// </summary>
[TestCategory(MSSQL_ENVIRONMENT)]
[TestMethod]
public async Task HotReloadAutoentities()
{
// Arrange
_writer = new StringWriter();
Console.SetOut(_writer);

// Act
HttpResponseMessage restResult = await _testClient.GetAsync($"rest/autoentity_books");

GenerateConfigFile(
connectionString: $"{ConfigurationTests.GetConnectionStringFromEnvironmentConfig(TestCategory.MSSQL).Replace("\\", "\\\\")}",
autoentityName: "HotReload_{object}");
await WaitForConditionAsync(
() => WriterContains(HOT_RELOAD_SUCCESS_MESSAGE),
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS),
TimeSpan.FromMilliseconds(500));

HttpResponseMessage failRestResult = await _testClient.GetAsync($"rest/autoentity_books");
HttpResponseMessage hotReloadRestResult = await _testClient.GetAsync($"rest/HotReload_books");

// Assert
Assert.AreEqual(HttpStatusCode.OK, restResult.StatusCode,
$"REST request before hot-reload failed when it was expected to succeed. Response: {await restResult.Content.ReadAsStringAsync()}");
Assert.AreEqual(HttpStatusCode.NotFound, failRestResult.StatusCode,
$"REST request after hot-reload succeeded when it was expected to fail. Response: {await failRestResult.Content.ReadAsStringAsync()}");
Assert.AreEqual(HttpStatusCode.OK, hotReloadRestResult.StatusCode,
$"REST request after hot-reload failed when it was expected to succeed. Response: {await hotReloadRestResult.Content.ReadAsStringAsync()}");
}

/// <summary>
/// /// (Warning: This test only currently works in the pipeline due to constrains of not
/// being able to change from one database type to another, under normal circumstances
Expand Down
2 changes: 2 additions & 0 deletions src/Service.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public static void Init()
VerifierSettings.IgnoreMember<Entity>(entity => entity.EntityFirst);
// Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint.
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsLinkingEntity);
// Ignore the entity IsAutoentity as that's unimportant from a test standpoint.
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsAutoentity);
// Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedTtlOptions);
// Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
Expand Down
Loading