in src/Core/Services/MultipleMutationInputValidator.cs [326:470]
private void ProcessRelationshipField(
IMiddlewareContext context,
ISqlMetadataProvider metadataProvider,
Dictionary<string, IValueNode?> backingColumnData,
Dictionary<string, string> derivableColumnsFromRequestBody,
Dictionary<string, HashSet<string>> fieldsToSupplyToReferencingEntities,
Dictionary<string, HashSet<string>> fieldsToDeriveFromReferencedEntities,
string relationshipName,
IValueNode? relationshipFieldValue,
int nestingLevel,
MultipleMutationEntityInputValidationContext multipleMutationEntityInputValidationContext)
{
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
string entityName = multipleMutationEntityInputValidationContext.EntityName;
// When the source of a referencing field in current entity is a relationship, the relationship's name is added to the value in
// the KV pair of (referencing column, source) in derivableColumnsFromRequestBody with a prefix '$' so that if the relationship name
// conflicts with the current entity's name or the parent entity's name, we are able to distinguish
// with the help of this identifier. It should be noted that the identifier is not allowed in the names
// of entities exposed in DAB.
const string relationshipSourceIdentifier = "$";
string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity;
string? linkingObject = runtimeConfig.Entities![entityName].Relationships![relationshipName].LinkingObject;
bool isMNRelationship = !string.IsNullOrWhiteSpace(linkingObject);
// Determine the referencing entity for the current relationship field input.
string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName(
relationshipName: relationshipName,
context: context,
sourceEntityName: entityName,
targetEntityName: targetEntityName,
metadataProvider: metadataProvider,
columnDataInSourceBody: backingColumnData,
targetNodeValue: relationshipFieldValue,
nestingLevel: nestingLevel,
isMNRelationship: isMNRelationship);
if (isMNRelationship)
{
// The presence of a linking object indicates that an M:N relationship exists between the current entity and the target/child entity.
// The linking table acts as a referencing table for both the source/target entities which act as
// referenced entities. Consequently:
// - Column values for the target entity can't be derived from insertion in the current entity.
// - Column values for the current entity can't be derived from the insertion in the target/child entity.
return;
}
// Determine the referenced entity.
string referencedEntityName = referencingEntityName.Equals(entityName) ? targetEntityName : entityName;
// Get the required foreign key definition with the above inferred referencing and referenced entities.
if (!metadataProvider.TryGetFKDefinition(
sourceEntityName: entityName,
targetEntityName: targetEntityName,
referencingEntityName: referencingEntityName,
referencedEntityName: referencedEntityName,
foreignKeyDefinition: out ForeignKeyDefinition? fkDefinition,
isMToNRelationship: false))
{
// This should not be hit ideally.
throw new DataApiBuilderException(
message: $"Could not resolve relationship metadata for source: {entityName} and target: {targetEntityName} entities for " +
$"relationship: {relationshipName} at level: {nestingLevel}",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.RelationshipNotFound);
}
// Validate that one column in the referencing entity is not referencing multiple columns in the referenced entity
// to avoid conflicting sources of truth for the value of referencing column.
IEnumerable<string> listOfRepeatedReferencingFields = GetListOfRepeatedExposedReferencingColumns(
referencingEntityName: referencingEntityName,
referencingColumns: fkDefinition.ReferencingColumns,
metadataProvider: metadataProvider);
if (listOfRepeatedReferencingFields.Count() > 0)
{
string repeatedReferencingFields = "{" + string.Join(", ", listOfRepeatedReferencingFields) + "}";
// This indicates one column is holding reference to multiple referenced columns in the related entity,
// which leads to possibility of two conflicting sources of truth for this column.
// This is an invalid use case for multiple-create.
throw new DataApiBuilderException(
message: $"The field(s): {repeatedReferencingFields} in the entity: {referencingEntityName} reference(s) multiple field(s) in the " +
$"related entity: {referencedEntityName} for the relationship: {relationshipName} at level: {nestingLevel}.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
// The current entity is the referencing entity.
if (referencingEntityName.Equals(entityName))
{
for (int idx = 0; idx < fkDefinition.ReferencingColumns.Count; idx++)
{
string referencingColumn = fkDefinition.ReferencingColumns[idx];
string referencedColumn = fkDefinition.ReferencedColumns[idx];
// The input data for current entity should not specify a value for a referencing column -
// as it's value will be derived from the insertion in the referenced (target) entity.
if (derivableColumnsFromRequestBody.TryGetValue(referencingColumn, out string? referencingColumnSource))
{
string conflictingSource;
if (referencingColumnSource.StartsWith(relationshipSourceIdentifier))
{
// If the source name starts with "$", this indicates the source for the referencing column
// was another relationship.
conflictingSource = "Relationship: " + referencingColumnSource.Substring(relationshipSourceIdentifier.Length);
}
else
{
conflictingSource = referencingColumnSource.Equals(multipleMutationEntityInputValidationContext.ParentEntityName) ? $"Parent entity: {referencingColumnSource}" : $"entity: {entityName}";
}
metadataProvider.TryGetExposedColumnName(entityName, referencingColumn, out string? exposedColumnName);
throw new DataApiBuilderException(
message: $"Found conflicting sources providing a value for the field: {exposedColumnName} for entity: {entityName} at level: {nestingLevel}." +
$"Source 1: {conflictingSource}, Source 2: Relationship: {relationshipName}.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
// Case 2: When a column whose value is to be derived from the insertion in current entity
// (happens when the parent entity is a referencing entity in a relationship with current entity),
// is a referencing column in the current relationship, we pass on the responsibility of getting the value
// of such a column to the target entity in the current relationship.
if (multipleMutationEntityInputValidationContext.ColumnsToBeDerivedFromEntity.Contains(referencingColumn))
{
// We optimistically assume that we will get the value of the referencing column
// from the insertion in the target entity.
multipleMutationEntityInputValidationContext.ColumnsToBeDerivedFromEntity.Remove(referencingColumn);
}
// Resolve the field(s) whose value(s) will be sourced from the creation of record in the current relationship's target entity.
fieldsToDeriveFromReferencedEntities.TryAdd(relationshipName, new());
fieldsToDeriveFromReferencedEntities[relationshipName].Add(referencedColumn);
// Value(s) for the current entity's referencing column(s) are sourced from the creation
// of record in the current relationship's target entity.
derivableColumnsFromRequestBody.TryAdd(referencingColumn, relationshipSourceIdentifier + relationshipName);
}
}
else
{
// Keep track of the set of referencing columns in the target (referencing) entity which will get their value sourced from insertion
// in the current entity.
fieldsToSupplyToReferencingEntities.Add(relationshipName, new(fkDefinition.ReferencingColumns));
}
}