in src/Core/Configurations/RuntimeConfigValidator.cs [227:324]
public void ValidateRelationshipConfigCorrectness(RuntimeConfig runtimeConfig)
{
// 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;
}
if (entity.Source.Type is not EntitySourceType.Table && entity.Relationships is not null
&& entity.Relationships.Count > 0)
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Cannot define relationship for entity: {entityName}",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships!)
{
// Validate if entity referenced in relationship is defined in the config.
if (!runtimeConfig.Entities.TryGetValue(relationship.TargetEntity, out Entity? targetEntity))
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Entity: {relationship.TargetEntity} used for relationship is not defined in the config.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
// Validation to ensure that an entity with graphQL disabled cannot be referenced in a relationship by other entities
EntityGraphQLOptions? targetEntityGraphQLDetails = targetEntity is not null ? targetEntity.GraphQL : null;
if (targetEntityGraphQLDetails is not null && !targetEntityGraphQLDetails.Enabled)
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Entity: {relationship.TargetEntity} is disabled for GraphQL.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
// Linking object is null and therefore we have a many-one or a one-many relationship. These relationships
// must be validated separately from many-many relationships. In one-many and many-one relationships, the count
// of source and target fields need to match, or if one is null the other must be as well.
// If both of these sets of fields are null, foreign key information, inferred from the database metadata,
// will be used to define the relationship.
// see: https://learn.microsoft.com/en-us/azure/data-api-builder/relationships
if (string.IsNullOrWhiteSpace(relationship.LinkingObject))
{
// Validation to ensure that source and target fields are both null or both not null.
if (relationship.SourceFields is null ^ relationship.TargetFields is null)
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Entity: {entityName} has a relationship: {relationshipName}, which has source and target fields " +
$"where one is null and the other is not.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
// In one-one, one-many, or many-one relationships when both source and target are non null their size must match.
if ((relationship.SourceFields is not null && relationship.TargetFields is not null) &&
relationship.SourceFields.Length != relationship.TargetFields.Length)
{
HandleOrRecordException(new DataApiBuilderException(
message: $"Entity: {entityName} has a relationship: {relationshipName}, which has {relationship.SourceFields.Length} source fields defined, " +
$"but {relationship.TargetFields.Length} target fields defined.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
}
// Linking object exists and we therefore have a many-many relationship. Validation here differs from one-many and many-one in that
// the source and target fields are now only indirectly related through the linking object. Therefore, it is the source and linkingSource
// along with the target and linkingTarget fields that must match, respectively. Source and linkingSource fields provide the relationship
// from the source entity to the linkingObject while target and linkingTarget fields provide the relationship from the target entity to the
// linkingObject.
// see: https://learn.microsoft.com/en-us/azure/data-api-builder/relationships#many-to-many-relationship
if (!string.IsNullOrWhiteSpace(relationship.LinkingObject))
{
ValidateFieldsAndAssociatedLinkingFields(
fields: relationship.SourceFields,
linkingFields: relationship.LinkingSourceFields,
fieldType: "source",
entityName: entityName,
relationshipName: relationshipName);
ValidateFieldsAndAssociatedLinkingFields(
fields: relationship.TargetFields,
linkingFields: relationship.LinkingTargetFields,
fieldType: "target",
entityName: entityName,
relationshipName: relationshipName);
}
}
}
}