Skip to content
Merged
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
35 changes: 31 additions & 4 deletions src/Core/Configurations/RuntimeConfigValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -806,15 +806,15 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, IMetadata
&& entity.Relationships.Count > 0)
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Cannot define relationship for entity: {entity}",
message: $"Cannot define relationship for entity: {entityName}",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}

string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
ISqlMetadataProvider sqlMetadataProvider = sqlMetadataProviderFactory.GetMetadataProvider(databaseName);

foreach (EntityRelationship relationship in entity.Relationships!.Values)
foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships!)
{
// Validate if entity referenced in relationship is defined in the config.
if (!runtimeConfig.Entities.ContainsKey(relationship.TargetEntity))
Expand All @@ -835,8 +835,35 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, IMetadata
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}

DatabaseTable sourceDatabaseObject = (DatabaseTable)sqlMetadataProvider.EntityToDatabaseObject[entityName];
DatabaseTable targetDatabaseObject = (DatabaseTable)sqlMetadataProvider.EntityToDatabaseObject[relationship.TargetEntity];
// Validation to ensure DatabaseObject is correctly inferred from the entity name.
DatabaseObject? sourceObject, targetObject;
if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out sourceObject))
{
sourceObject = null;
HandleOrRecordException(new DataApiBuilderException(
message: $"Could not infer database object for source entity: {entityName} in relationship: {relationshipName}." +
$" Check if the entity: {entityName} is correctly defined in the config.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}

if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(relationship.TargetEntity, out targetObject))
{
targetObject = null;
HandleOrRecordException(new DataApiBuilderException(
message: $"Could not infer database object for target entity: {relationship.TargetEntity} in relationship: {relationshipName}." +
$" Check if the entity: {relationship.TargetEntity} is correctly defined in the config.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}

if (sourceObject is null || targetObject is null)
{
continue;
}

DatabaseTable sourceDatabaseObject = (DatabaseTable)sourceObject;
DatabaseTable targetDatabaseObject = (DatabaseTable)targetObject;
if (relationship.LinkingObject is not null)
{
(string linkingTableSchema, string linkingTableName) = sqlMetadataProvider.ParseSchemaAndDbTableName(relationship.LinkingObject)!;
Expand Down
68 changes: 51 additions & 17 deletions src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,26 +333,37 @@ private void LogPrimaryKeys()
ColumnDefinition column;
foreach ((string entityName, Entity _) in _entities)
{
SourceDefinition sourceDefinition = GetSourceDefinition(entityName);
_logger.LogDebug("Logging primary key information for entity: {entityName}.", entityName);
foreach (string pK in sourceDefinition.PrimaryKey)
try
{
column = sourceDefinition.Columns[pK];
if (TryGetExposedColumnName(entityName, pK, out string? exposedPKeyName))
SourceDefinition sourceDefinition = GetSourceDefinition(entityName);
_logger.LogDebug("Logging primary key information for entity: {entityName}.", entityName);
foreach (string pK in sourceDefinition.PrimaryKey)
{
_logger.LogDebug(
message: "Primary key column name: {pK}\n" +
" Primary key mapped name: {exposedPKeyName}\n" +
" Type: {column.SystemType.Name}\n" +
" IsNullable: {column.IsNullable}\n" +
" IsAutoGenerated: {column.IsAutoGenerated}",
pK,
exposedPKeyName,
column.SystemType.Name,
column.IsNullable,
column.IsAutoGenerated);
column = sourceDefinition.Columns[pK];
if (TryGetExposedColumnName(entityName, pK, out string? exposedPKeyName))
{
_logger.LogDebug(
message: "Primary key column name: {pK}\n" +
" Primary key mapped name: {exposedPKeyName}\n" +
" Type: {column.SystemType.Name}\n" +
" IsNullable: {column.IsNullable}\n" +
" IsAutoGenerated: {column.IsAutoGenerated}",
pK,
exposedPKeyName,
column.SystemType.Name,
column.IsNullable,
column.IsAutoGenerated);
}
}
}
catch (Exception ex)
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Failed to log primary key information for entity: {entityName} due to: {ex.Message}",
innerException: ex,
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization));
}
}
}

Expand Down Expand Up @@ -619,6 +630,14 @@ private void GenerateDatabaseObjectForEntities()

if (!EntityToDatabaseObject.ContainsKey(entityName))
{
if (entity.Source.Object is null)
{
throw new DataApiBuilderException(
message: $"The entity {entityName} does not have a valid source object.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}

// Reuse the same Database object for multiple entities if they share the same source.
if (!sourceObjects.TryGetValue(entity.Source.Object, out DatabaseObject? sourceObject))
{
Expand Down Expand Up @@ -730,6 +749,14 @@ private void AddForeignKeysForRelationships(
throw new InvalidOperationException($"Target Entity {targetEntityName} should be one of the exposed entities.");
}

if (targetEntity.Source.Object is null)
{
throw new DataApiBuilderException(
message: $"Target entity {entityName} does not have a valid source object.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}

(targetSchemaName, targetDbTableName) = ParseSchemaAndDbTableName(targetEntity.Source.Object)!;
DatabaseTable targetDbTable = new(targetSchemaName, targetDbTableName);
// If a linking object is specified,
Expand Down Expand Up @@ -963,7 +990,14 @@ await PopulateSourceDefinitionAsync(
}
}

await PopulateForeignKeyDefinitionAsync();
try
{
await PopulateForeignKeyDefinitionAsync();
}
catch (Exception e)
{
HandleOrRecordException(e);
}
}

/// <summary>
Expand Down
88 changes: 88 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,94 @@ public async Task TestSqlMetadataForInvalidConfigEntities()
Assert.AreEqual("No stored procedure definition found for the given database object publishers", exceptionsList[1].Message);
}

/// <summary>
/// This Test validates that when the entities in the runtime config have source object as null,
/// the validation exception handler collects the message and exits gracefully.
/// </summary>
[TestMethod("Validate Exception handling for Entities with Source object as null."), TestCategory(TestCategory.MSSQL)]
public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource()
{
TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT);

DataSource dataSource = new(DatabaseType.MSSQL,
GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL),
Options: null);

RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new());

// creating an entity with invalid table name
Entity entityWithInvalidSource = new(
Source: new(null, EntitySourceType.Table, null, null),
Rest: null,
GraphQL: new(Singular: "book", Plural: "books"),
Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: null,
Mappings: null
);

// creating an entity with invalid source object and adding relationship with an entity with invalid source
Entity entityWithInvalidSourceAndRelationship = new(
Source: new(null, EntitySourceType.Table, null, null),
Rest: null,
GraphQL: new(Singular: "publisher", Plural: "publishers"),
Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: new Dictionary<string, EntityRelationship>() { {"books", new (
Cardinality: Cardinality.Many,
TargetEntity: "Book",
SourceFields: null,
TargetFields: null,
LinkingObject: null,
LinkingSourceFields: null,
LinkingTargetFields: null
)}},
Mappings: null
);

configuration = configuration with
{
Entities = new RuntimeEntities(new Dictionary<string, Entity>()
{
{ "Book", entityWithInvalidSource },
{ "Publisher", entityWithInvalidSourceAndRelationship}
})
};

const string CUSTOM_CONFIG = "custom-config.json";
File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson());

FileSystemRuntimeConfigLoader configLoader = TestHelper.GetRuntimeConfigLoader();
configLoader.UpdateConfigFilePath(CUSTOM_CONFIG);
RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(configLoader);

Mock<ILogger<RuntimeConfigValidator>> configValidatorLogger = new();
RuntimeConfigValidator configValidator =
new(
configProvider,
new MockFileSystem(),
configValidatorLogger.Object,
isValidateOnly: true);

ILoggerFactory mockLoggerFactory = TestHelper.ProvisionLoggerFactory();

try
{
await configValidator.ValidateEntitiesMetadata(configProvider.GetConfig(), mockLoggerFactory);
}
catch
{
Assert.Fail("Execution of dab validate should not result in unhandled exceptions.");
}

Assert.IsTrue(configValidator.ConfigValidationExceptions.Any());
List<string> exceptionMessagesList = configValidator.ConfigValidationExceptions.Select(x => x.Message).ToList();
Assert.IsTrue(exceptionMessagesList.Contains("The entity Book does not have a valid source object."));
Assert.IsTrue(exceptionMessagesList.Contains("The entity Publisher does not have a valid source object."));
Assert.IsTrue(exceptionMessagesList.Contains("Table Definition for Book has not been inferred."));
Assert.IsTrue(exceptionMessagesList.Contains("Table Definition for Publisher has not been inferred."));
Assert.IsTrue(exceptionMessagesList.Contains("Could not infer database object for source entity: Publisher in relationship: books. Check if the entity: Publisher is correctly defined in the config."));
Assert.IsTrue(exceptionMessagesList.Contains("Could not infer database object for target entity: Book in relationship: books. Check if the entity: Book is correctly defined in the config."));
}

/// <summary>
/// This test method validates a sample DAB runtime config file against DAB's JSON schema definition.
/// It asserts that the validation is successful and there are no validation failures.
Expand Down