in src/Core/Configurations/RuntimeConfigValidator.cs [876:1044]
public void ValidateRelationships(RuntimeConfig runtimeConfig, IMetadataProviderFactory sqlMetadataProviderFactory)
{
// To avoid creating many lists of invalid columns we instantiate before looping through entities.
// List.Clear() is O(1) so clearing the list, for re-use, inside of the loops is fine.
List<string> invalidColumns = new();
// Loop through each entity in the config and verify its relationship.
foreach ((string entityName, Entity entity) in runtimeConfig.Entities)
{
// Skipping relationship validation if entity has no relationship
// or if graphQL is disabled.
if (entity.Relationships is null || !entity.GraphQL.Enabled)
{
continue;
}
string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
ISqlMetadataProvider sqlMetadataProvider = sqlMetadataProviderFactory.GetMetadataProvider(databaseName);
foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships!)
{
ValidateSourceAndTargetFieldsAsBackingColumns(
entityName: entityName,
relationshipName: relationshipName,
relationship: relationship,
sqlMetadataProvider: sqlMetadataProvider,
invalidColumns: invalidColumns);
// 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)!;
DatabaseTable linkingDatabaseObject = new(linkingTableSchema, linkingTableName);
if (relationship.LinkingSourceFields is null || relationship.SourceFields is null)
{
if (!sqlMetadataProvider.VerifyForeignKeyExistsInDB(linkingDatabaseObject, sourceDatabaseObject))
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Could not find relationship between Linking Object: {relationship.LinkingObject}" +
$" and entity: {entityName}.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
}
if (relationship.LinkingTargetFields is null || relationship.TargetFields is null)
{
if (!sqlMetadataProvider.VerifyForeignKeyExistsInDB(linkingDatabaseObject, targetDatabaseObject))
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Could not find relationship between Linking Object: {relationship.LinkingObject}" +
$" and entity: {relationship.TargetEntity}.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
}
if (!_runtimeConfigProvider.IsLateConfigured)
{
string sourceDBOName = sqlMetadataProvider.EntityToDatabaseObject[entityName].FullName;
string targetDBOName = sqlMetadataProvider.EntityToDatabaseObject[relationship.TargetEntity].FullName;
string cardinality = relationship.Cardinality.ToString().ToLower();
RelationShipPair linkedSourceRelationshipPair = new(linkingDatabaseObject, sourceDatabaseObject);
RelationShipPair linkedTargetRelationshipPair = new(linkingDatabaseObject, targetDatabaseObject);
ForeignKeyDefinition? fKDef;
string referencedSourceColumns = relationship.SourceFields is not null ? string.Join(",", relationship.SourceFields) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedSourceRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencedColumns) : string.Empty;
string referencingSourceColumns = relationship.LinkingSourceFields is not null ? string.Join(",", relationship.LinkingSourceFields) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedSourceRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencingColumns) : string.Empty;
string referencedTargetColumns = relationship.TargetFields is not null ? string.Join(",", relationship.TargetFields) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedTargetRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencedColumns) : string.Empty;
string referencingTargetColumns = relationship.LinkingTargetFields is not null ? string.Join(",", relationship.LinkingTargetFields) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedTargetRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencingColumns) : string.Empty;
_logger.LogDebug(
message: "{entityName}: {sourceDBOName}({referencedSourceColumns}) is related to {cardinality} " +
"{relationship.TargetEntity}: {targetDBOName}({referencedTargetColumns}) by " +
"{relationship.LinkingObject}(linking.source.fields: {referencingSourceColumns}), (linking.target.fields: {referencingTargetColumns})",
entityName,
sourceDBOName,
referencedSourceColumns,
cardinality,
relationship.TargetEntity,
targetDBOName,
referencedTargetColumns,
relationship.LinkingObject,
referencingSourceColumns,
referencingTargetColumns);
}
}
if (relationship.LinkingObject is null
&& (relationship.SourceFields is null || relationship.TargetFields is null))
{
if (!sqlMetadataProvider.VerifyForeignKeyExistsInDB(sourceDatabaseObject, targetDatabaseObject))
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Could not find relationship between entities: {entityName} and {relationship.TargetEntity}.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
}
if (relationship.LinkingObject is null && !_runtimeConfigProvider.IsLateConfigured)
{
RelationShipPair sourceTargetRelationshipPair = new(sourceDatabaseObject, targetDatabaseObject);
RelationShipPair targetSourceRelationshipPair = new(targetDatabaseObject, sourceDatabaseObject);
string sourceDBOName = sqlMetadataProvider.EntityToDatabaseObject[entityName].FullName;
string targetDBOName = sqlMetadataProvider.EntityToDatabaseObject[relationship.TargetEntity].FullName;
string cardinality = relationship.Cardinality.ToString().ToLower();
ForeignKeyDefinition? fKDef;
string sourceColumns = relationship.SourceFields is not null ? string.Join(",", relationship.SourceFields) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(sourceTargetRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencingColumns) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(targetSourceRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencedColumns) : string.Empty;
string targetColumns = relationship.TargetFields is not null ? string.Join(",", relationship.TargetFields) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(sourceTargetRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencedColumns) :
sqlMetadataProvider.PairToFkDefinition!.TryGetValue(targetSourceRelationshipPair, out fKDef) ?
string.Join(",", fKDef.ReferencingColumns) : string.Empty;
_logger.LogDebug(
message: "{entityName}: {sourceDBOName}({sourceColumns}) is related to {cardinality} {relationshipTargetEntity}: {targetDBOName}({targetColumns}).",
entityName,
sourceDBOName,
sourceColumns,
cardinality,
relationship.TargetEntity,
targetDBOName,
targetColumns
);
}
}
}
}