private void ValidateObjectFieldNodes()

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);
        }