in src/Core/Services/MultipleMutationInputValidator.cs [142:290]
private void ValidateObjectFieldNodes(
InputObjectType schemaObject,
IMiddlewareContext context,
IReadOnlyList<ObjectFieldNode> currentEntityInputFieldNodes,
int nestingLevel,
MultipleMutationEntityInputValidationContext multipleMutationEntityInputValidationContext)
{
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
string entityName = multipleMutationEntityInputValidationContext.EntityName;
string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, runtimeConfig);
ISqlMetadataProvider metadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName);
SourceDefinition sourceDefinition = metadataProvider.GetSourceDefinition(entityName);
Dictionary<string, IValueNode?> backingColumnData = MultipleCreateOrderHelper.GetBackingColumnDataFromFields(context, entityName, currentEntityInputFieldNodes, metadataProvider);
// Set of columns in the current entity whose values can be derived via:
// a. User input
// b. Insertion in the source referenced entity (if current entity is a referencing entity for a relationship with its parent entity)
// c. Insertion in the target referenced entity (if the current entity is a referencing entity for a relationship with its target entity)
//
// derivableColumnsFromRequestBody is initialized with the set of columns whose value is specified in the input (a).
Dictionary<string, string> derivableColumnsFromRequestBody = new();
foreach ((string backingColumnName, _) in backingColumnData)
{
derivableColumnsFromRequestBody.TryAdd(backingColumnName, entityName);
}
// When the parent entity is a referenced entity in a relationship, the values of the referencing columns
// in the current entity are derived from the insertion in the parent entity. Hence, the input data for
// current entity (referencing entity) i.e. derivableColumnsFromRequestBody must not contain values for referencing columns.
ValidateAbsenceOfReferencingColumnsInTargetEntity(
backingColumnToSourceMapInTargetEntity: derivableColumnsFromRequestBody,
multipleMutationInputValidationContext: multipleMutationEntityInputValidationContext,
nestingLevel: nestingLevel,
metadataProvider: metadataProvider);
// Add all the columns whose value(s) will be derived from insertion in parent entity to the set of derivable columns (b).
foreach (string columnDerivedFromSourceEntity in multipleMutationEntityInputValidationContext.ColumnsDerivedFromParentEntity)
{
derivableColumnsFromRequestBody.TryAdd(columnDerivedFromSourceEntity, multipleMutationEntityInputValidationContext.ParentEntityName);
}
// For the relationships with the parent entity, where the current entity is a referenced entity,
// we need to make sure that we have non-null values for all the referenced columns - since the values for all those
// columns will be used for insertion in the parent entity. We can get the referenced column value:
// Case 1. Via a scalar value provided in the input for the current entity
// Case 2. Via another relationship where a column referenced by the parent entity (let's say A) of the current entity (let's say B)
// is a referencing column for a target entity (let's say C) related with current entity.
// Eg. Suppose there are 3 entities A,B,C which appear in the same order in the multiple mutation.
// The relationships defined between the entities are:
// A.id (referencing) -> B.id (referenced)
// B.id (referencing) -> C.id (referenced)
// The value for A.id can be derived from insertion in the table B. B.id in turn can be derived from insertion in the table C.
// So, for a mutation like:
// mutation {
// createA(item: { aname: "abc", B: { bname: "abc", C: { cname: "abc" } } }) {
// id
// }
// }
// the value for A.id, in effect, will be derived from C.id.
// Case 1: Remove from columnsToBeDerivedFromEntity, the columns which are autogenerated or
// have been provided a non-null value in the input for the current entity.
// As we iterate through columns which can be derived from the current entity, we keep removing them from the
// columnsToBeSuppliedToSourceEntity because we know the column value can be derived.
// At the end, if there are still columns yet to be derived, i.e. columnsToBeSuppliedToSourceEntity.count > 0,
// we throw an exception.
foreach (string columnToBeDerivedFromEntity in multipleMutationEntityInputValidationContext.ColumnsToBeDerivedFromEntity)
{
if (sourceDefinition.Columns[columnToBeDerivedFromEntity].IsAutoGenerated)
{
// The value for an autogenerated column is derivable. In other words, the value autogenerated during creation of this entity
// can be provided to another entity's referencing fields.
multipleMutationEntityInputValidationContext.ColumnsToBeDerivedFromEntity.Remove(columnToBeDerivedFromEntity);
}
else if (backingColumnData.TryGetValue(columnToBeDerivedFromEntity, out IValueNode? value))
{
if (value is null)
{
metadataProvider.TryGetExposedColumnName(entityName, columnToBeDerivedFromEntity, out string? exposedColumnName);
throw new DataApiBuilderException(
message: $"Value cannot be null for referenced field: {exposedColumnName} for entity: {entityName} at level: {nestingLevel}.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
multipleMutationEntityInputValidationContext.ColumnsToBeDerivedFromEntity.Remove(columnToBeDerivedFromEntity);
}
}
// Dictionary storing the mapping from relationship name to the set of referencing fields for all
// the relationships for which the current entity is the referenced entity.
Dictionary<string, HashSet<string>> fieldsToSupplyToReferencingEntities = new();
// Dictionary storing the mapping from relationship name to the set of referenced fields for all
// the relationships for which the current entity is the referencing entity.
Dictionary<string, HashSet<string>> fieldsToDeriveFromReferencedEntities = new();
// Loop over all the relationship fields input provided for the current entity.
foreach (ObjectFieldNode currentEntityInputFieldNode in currentEntityInputFieldNodes)
{
(IValueNode? fieldValue, SyntaxKind fieldKind) = GraphQLUtils.GetFieldDetails(currentEntityInputFieldNode.Value, context.Variables);
if (fieldKind is not SyntaxKind.NullValue && !GraphQLUtils.IsScalarField(fieldKind))
{
// Process the relationship field.
string relationshipName = currentEntityInputFieldNode.Name.Value;
ProcessRelationshipField(
context: context,
metadataProvider: metadataProvider,
backingColumnData: backingColumnData,
derivableColumnsFromRequestBody: derivableColumnsFromRequestBody,
fieldsToSupplyToReferencingEntities: fieldsToSupplyToReferencingEntities,
fieldsToDeriveFromReferencedEntities: fieldsToDeriveFromReferencedEntities,
relationshipName: relationshipName,
relationshipFieldValue: fieldValue,
nestingLevel: nestingLevel,
multipleMutationEntityInputValidationContext: multipleMutationEntityInputValidationContext);
}
}
// After determining all the fields that can be derived for the current entity either:
// 1. Via absolute value,
// 2. Via an autogenerated value from the database,
// 3. Via Insertion in a referenced target entity in a relationship,
// if there are still columns which are yet to be derived, this means we don't have sufficient data to perform insertion.
if (multipleMutationEntityInputValidationContext.ColumnsToBeDerivedFromEntity.Count > 0)
{
throw new DataApiBuilderException(
message: $"Insufficient data provided for insertion in the entity: {entityName} at level: {nestingLevel}.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
// For multiple-create, we generate the schema such that the foreign key referencing columns become optional i.e.,
// 1. Either the client provides the values (when it is a point create), or
// 2. We derive the values via insertion in the referenced entity.
// But we need to ensure that we either have a source (either via 1 or 2) for all the required columns required to do a successful insertion.
ValidatePresenceOfRequiredColumnsForInsertion(derivableColumnsFromRequestBody.Keys.ToHashSet(), entityName, metadataProvider, nestingLevel);
// Recurse to validate input data for the relationship fields.
ValidateRelationshipFields(
schemaObject: schemaObject,
entityName: entityName,
context: context,
currentEntityInputFields: currentEntityInputFieldNodes,
fieldsToSupplyToReferencingEntities: fieldsToSupplyToReferencingEntities,
fieldsToDeriveFromReferencedEntities: fieldsToDeriveFromReferencedEntities,
nestingLevel: nestingLevel);
}