src/Core/Services/MultipleMutationInputValidator.cs (349 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Net;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Core.Services.MetadataProviders;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder;
using Azure.DataApiBuilder.Service.Services;
using HotChocolate.Language;
using HotChocolate.Resolvers;
namespace Azure.DataApiBuilder.Core.Services
{
public class MultipleMutationInputValidator
{
private readonly RuntimeConfigProvider _runtimeConfigProvider;
private readonly IMetadataProviderFactory _sqlMetadataProviderFactory;
public MultipleMutationInputValidator(IMetadataProviderFactory sqlMetadataProviderFactory, RuntimeConfigProvider runtimeConfigProvider)
{
_sqlMetadataProviderFactory = sqlMetadataProviderFactory;
_runtimeConfigProvider = runtimeConfigProvider;
}
/// <summary>
/// Recursive method to validate a GraphQL value node which can represent the input for item/items
/// argument for the mutation node, or can represent an object value (*:1 relationship)
/// or a list value (*:N relationship) node for related target entities.
/// </summary>
/// <param name="schema">Schema for the input field</param>
/// <param name="context">Middleware Context.</param>
/// <param name="parameters">Value for the input field.</param>
/// <param name="nestingLevel">Current depth of nesting in the multiple-create request. As we go down in the multiple mutation from the source
/// entity input to target entity input, the nesting level increases by 1. This helps us to throw meaningful exception messages to the user
/// as to at what level in the multiple mutation the exception occurred.</param>
/// <param name="multipleMutationEntityInputValidationContext">MultipleMutationInputValidationContext object for current entity.</param>
/// <example> 1. mutation {
/// createbook(
/// item: {
/// title: "book #1",
/// reviews: [{ content: "Good book." }, { content: "Great book." }],
/// publisher: { name: "Macmillan publishers" },
/// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }]
/// })
/// {
/// id
/// }
/// 2. mutation {
/// createbooks(
/// items: [{
/// title: "book #1",
/// reviews: [{ content: "Good book." }, { content: "Great book." }],
/// publisher: { name: "Macmillan publishers" },
/// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }]
/// },
/// {
/// title: "book #2",
/// reviews: [{ content: "Awesome book." }, { content: "Average book." }],
/// publisher: { name: "Pearson Education" },
/// authors: [{ birthdate: "1990-11-04", name: "Penguin Random House", author_name: "William Shakespeare" }]
/// }])
/// {
/// items{
/// id
/// title
/// }
/// }</example>
public void ValidateGraphQLValueNode(
IInputField schema,
IMiddlewareContext context,
object? parameters,
int nestingLevel,
MultipleMutationEntityInputValidationContext multipleMutationEntityInputValidationContext)
{
if (parameters is List<ObjectFieldNode> listOfObjectFieldNodes)
{
// For the example createbook mutation written above, the object value for `item` is interpreted as a List<ObjectFieldNode> i.e.
// all the fields present for item namely- title, reviews, publisher, authors are interpreted as ObjectFieldNode.
ValidateObjectFieldNodes(
schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema),
context: context,
currentEntityInputFieldNodes: listOfObjectFieldNodes,
nestingLevel: nestingLevel + 1,
multipleMutationEntityInputValidationContext: multipleMutationEntityInputValidationContext);
}
else if (parameters is List<IValueNode> listOfIValueNodes)
{
// For the example createbooks mutation written above, the list value for `items` is interpreted as a List<IValueNode>
// i.e. items is a list of ObjectValueNode(s).
listOfIValueNodes.ForEach(iValueNode => ValidateGraphQLValueNode(
schema: schema,
context: context,
parameters: iValueNode,
nestingLevel: nestingLevel,
multipleMutationEntityInputValidationContext: multipleMutationEntityInputValidationContext));
}
else if (parameters is ObjectValueNode objectValueNode)
{
// For the example createbook mutation written above, the node for publisher field is interpreted as an ObjectValueNode.
// Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s).
ValidateObjectFieldNodes(
schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema),
context: context,
currentEntityInputFieldNodes: objectValueNode.Fields,
nestingLevel: nestingLevel + 1,
multipleMutationEntityInputValidationContext: multipleMutationEntityInputValidationContext);
}
else if (parameters is ListValueNode listValueNode)
{
// For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode.
// All the nodes in the ListValueNode are parsed one by one.
listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => ValidateGraphQLValueNode(
schema: schema,
context: context,
parameters: objectValueNodeInListValueNode,
nestingLevel: nestingLevel,
multipleMutationEntityInputValidationContext: multipleMutationEntityInputValidationContext));
}
else
{
throw new DataApiBuilderException(message: $"Unable to process input at level: {nestingLevel} for entity: {multipleMutationEntityInputValidationContext.EntityName}",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
}
}
/// <summary>
/// Helper method to iterate over all the fields present in the input for the current field and
/// validate that the presence/absence of fields make logical sense for the multiple-create request.
/// </summary>
/// <param name="schemaObject">Input object type for the field.</param>
/// <param name="context">Middleware Context.</param>
/// <param name="currentEntityInputFieldNodes">List of ObjectFieldNodes for the the input field.</param>
/// <param name="nestingLevel">Current depth of nesting in the multiple-create request. As we go down in the multiple mutation from the source
/// entity input to target entity input, the nesting level increases by 1. This helps us to throw meaningful exception messages to the user
/// as to at what level in the multiple mutation the exception occurred.</param>
/// <param name="multipleMutationEntityInputValidationContext">MultipleMutationInputValidationContext object for current entity.</param>
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);
}
/// <summary>
/// Helper method which processes a relationship field provided in the input for the current entity and does a few things:
/// 1. Does nothing when the relationship represents an M:N relationship as for M:N relationship both the source/target entities act as
/// referenced tables while the linking table act as the referencing table..
/// 2. Validates that one referencing column from referencing entity does not reference multiple columns from the referenced entity.
/// 3. When the target entity is the referenced entity:
/// - Validates that there are no conflicting sources of values for fields in the current entity. This happens when the insertion in the target
/// entity returns a value for a referencing field in current entity but the value for the same referencing field had already being determined
/// via insertion in the parent entity or the current entity.
/// - Adds a mapping from (relationshipName,referencedFields in target entity) to 'fieldsToDeriveFromReferencedEntities' for the current relationship.
/// - Adds to 'derivableColumnsFromRequestBody', the set of referencing fields for which values can be derived from insertion in the target entity.
/// - Removes the referencing fields present in multipleMutationInputValidationContext.ColumnsToBeDerivedFromEntity because the responsibility
/// of providing value for such fields is delegated to the target entity.
/// 4. When the target entity is the referencing entity, adds a mapping from (relationshipName,referencingFields in target entity) to
/// 'fieldsToSupplyToReferencingEntities' for the current relationship.
/// </summary>
/// <param name="context">Middleware context.</param>
/// <param name="metadataProvider">Metadata provider.</param>
/// <param name="backingColumnData">Column name/value for backing columns present in the request input for the current entity.</param>
/// <param name="derivableColumnsFromRequestBody">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).
/// </param>
/// <param name="fieldsToSupplyToReferencingEntities">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.</param>
/// <param name="fieldsToDeriveFromReferencedEntities">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.</param>
/// <param name="relationshipName">Relationship name.</param>
/// <param name="relationshipFieldValue">Input value for relationship field.</param>
/// <param name="nestingLevel">Current depth of nesting in the multiple-create request. As we go down in the multiple mutation from the source
/// entity input to target entity input, the nesting level increases by 1. This helps us to throw meaningful exception messages to the user
/// as to at what level in the multiple mutation the exception occurred.</param>
/// <param name="multipleMutationEntityInputValidationContext">MultipleMutationInputValidationContext object for current entity.</param>
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));
}
}
/// <summary>
/// Helper method get list of columns in a referencing entity which hold multiple references to a referenced entity.
/// We don't support multiple-create for such use cases because then we have conflicting sources of truth for values for
/// the columns holding multiple references to referenced entity. In such cases, the referencing column can assume the
/// value of any referenced column which leads to ambiguities as to what value to assign to the referencing column.
/// </summary>
/// <param name="referencingEntityName">Name of the referencing entity.</param>
/// <param name="referencingColumns">Set of referencing columns.</param>
/// <param name="metadataProvider">Metadata provider.</param>
private static IEnumerable<string> GetListOfRepeatedExposedReferencingColumns(
string referencingEntityName,
List<string> referencingColumns,
ISqlMetadataProvider metadataProvider)
{
HashSet<string> referencingFields = new();
List<string> repeatedReferencingFields = new();
foreach (string referencingColumn in referencingColumns)
{
if (referencingFields.Contains(referencingColumn))
{
metadataProvider.TryGetExposedColumnName(referencingEntityName, referencingColumn, out string? exposedReferencingColumnName);
repeatedReferencingFields.Add(exposedReferencingColumnName!);
}
referencingFields.Add(referencingColumn);
}
return repeatedReferencingFields;
}
/// <summary>
/// Helper method to validate that we have non-null values for all the fields which are non-nullable or do not have a
/// default value. With multiple-create, the fields which hold FK reference to other fields in same/other referenced table
/// become optional, because their values may come from insertion in the aforementioned referenced table. However, providing
/// data for insertion in the referenced table is again optional. Hence, the request input may not contain data for any of the
/// referencing fields or the referenced table. We need to invalidate such request.
/// </summary>
/// <param name="derivableBackingColumns">Set of backing columns in the current entity whose values can be derived.</param>
/// <param name="entityName">Name of the entity</param>
/// <param name="metadataProvider">Metadata provider.</param>
/// <param name="nestingLevel">Current depth of nesting in the multiple-create request.</param>
/// <example>mutation
/// {
/// createbook(item: { title: ""My New Book"" }) {
/// id
/// title
/// }
/// }
/// In the above example, the value for publisher_id column could not be derived because neither the relationship field
/// with Publisher entity was present which would give back the publisher_id nor do we have a scalar value provided by the
/// user for publisher_id.
/// </example>
private static void ValidatePresenceOfRequiredColumnsForInsertion(
HashSet<string> derivableBackingColumns,
string entityName,
ISqlMetadataProvider metadataProvider,
int nestingLevel)
{
SourceDefinition sourceDefinition = metadataProvider.GetSourceDefinition(entityName);
Dictionary<string, ColumnDefinition> columns = sourceDefinition.Columns;
foreach ((string columnName, ColumnDefinition columnDefinition) in columns)
{
// Must specify a value for a non-nullable column which does not have a default value.
if (!columnDefinition.IsNullable && !columnDefinition.HasDefault && !columnDefinition.IsAutoGenerated && !derivableBackingColumns.Contains(columnName))
{
metadataProvider.TryGetExposedColumnName(entityName, columnName, out string? exposedColumnName);
throw new DataApiBuilderException(
message: $"Missing value for required column: {exposedColumnName} for entity: {entityName} at level: {nestingLevel}.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
}
}
/// <summary>
/// Helper method to validate input data for the relationship fields present in the input for the current entity.
/// </summary>
/// <param name="schemaObject">Input object type for the field.</param>
/// <param name="entityName">Current entity's name.</param>
/// <param name="context">Middleware context.</param>
/// <param name="currentEntityInputFields">List of ObjectFieldNodes for the the input field.</param>
/// <param name="fieldsToSupplyToReferencingEntities">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.</param>
/// <param name="fieldsToDeriveFromReferencedEntities">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.</param>
/// <param name="nestingLevel">Current depth of nesting in the multiple-create request. As we go down in the multiple mutation from the source
/// entity input to target entity input, the nesting level increases by 1. This helps us to throw meaningful exception messages to the user
/// as to at what level in the multiple mutation the exception occurred.</param>
private void ValidateRelationshipFields(
InputObjectType schemaObject,
string entityName,
IMiddlewareContext context,
IReadOnlyList<ObjectFieldNode> currentEntityInputFields,
Dictionary<string, HashSet<string>> fieldsToSupplyToReferencingEntities,
Dictionary<string, HashSet<string>> fieldsToDeriveFromReferencedEntities,
int nestingLevel)
{
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
foreach (ObjectFieldNode currentEntityInputField in currentEntityInputFields)
{
Tuple<IValueNode?, SyntaxKind> fieldDetails = GraphQLUtils.GetFieldDetails(currentEntityInputField.Value, context.Variables);
SyntaxKind fieldKind = fieldDetails.Item2;
// For non-scalar fields, i.e. relationship fields, we have to recurse to process fields in the relationship field -
// which represents input data for a related entity.
if (fieldKind is not SyntaxKind.NullValue && !GraphQLUtils.IsScalarField(fieldKind))
{
string relationshipName = currentEntityInputField.Name.Value;
string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity;
HashSet<string>? columnValuePairsResolvedForTargetEntity, columnValuePairsToBeResolvedFromTargetEntity;
// When the current entity is a referenced entity, there will be no corresponding entry for the relationship
// in the fieldsToSupplyToReferencingEntities dictionary.
fieldsToSupplyToReferencingEntities.TryGetValue(relationshipName, out columnValuePairsResolvedForTargetEntity);
// When the current entity is a referencing entity, there will be no corresponding entry for the relationship
// in the fieldsToDeriveFromReferencedEntities dictionary.
fieldsToDeriveFromReferencedEntities.TryGetValue(relationshipName, out columnValuePairsToBeResolvedFromTargetEntity);
MultipleMutationEntityInputValidationContext multipleMutationEntityInputValidationContext = new(
entityName: targetEntityName,
parentEntityName: entityName,
columnsDerivedFromParentEntity: columnValuePairsResolvedForTargetEntity ?? new(),
columnsToBeDerivedFromEntity: columnValuePairsToBeResolvedFromTargetEntity ?? new());
ValidateGraphQLValueNode(
schema: schemaObject.Fields[relationshipName],
context: context,
parameters: fieldDetails.Item1,
nestingLevel: nestingLevel,
multipleMutationEntityInputValidationContext: multipleMutationEntityInputValidationContext);
}
}
}
/// <summary>
/// Helper method to validate that the referencing columns are not included in the input data for the target (referencing) entity -
/// because the value for such referencing columns is derived from the insertion in the source (referenced) entity.
/// In case when a value for referencing column is also specified in the referencing entity, there can be two conflicting sources of truth,
/// which we don't want to allow. In such a case, we throw an appropriate exception.
/// </summary>
/// <param name="backingColumnToSourceMapInTargetEntity">Dictionary storing mapping from names of backing columns in the target (referencing) entity to the source
/// of the corresponding column's value The source can either be:
/// 1. User provided scalar input for a column (in which case source value = current entity's name)
/// 2. Insertion in the source (referenced) entity (in which case source value = parent entity's name)</param>
/// <param name="nestingLevel">Current depth of nesting in the multiple-create GraphQL request.</param>
/// <param name="metadataProvider">Metadata provider.</param>
/// <param name="multipleMutationInputValidationContext">MultipleMutationInputValidationContext object for current entity.</param>
private static void ValidateAbsenceOfReferencingColumnsInTargetEntity(
Dictionary<string, string> backingColumnToSourceMapInTargetEntity,
int nestingLevel,
ISqlMetadataProvider metadataProvider,
MultipleMutationEntityInputValidationContext multipleMutationInputValidationContext)
{
foreach (string derivedColumnFromParentEntity in multipleMutationInputValidationContext.ColumnsDerivedFromParentEntity)
{
if (backingColumnToSourceMapInTargetEntity.ContainsKey(derivedColumnFromParentEntity))
{
metadataProvider.TryGetExposedColumnName(multipleMutationInputValidationContext.EntityName, derivedColumnFromParentEntity, out string? exposedColumnName);
throw new DataApiBuilderException(
message: $"You can't specify the field: {exposedColumnName} in the create input for entity: {multipleMutationInputValidationContext.EntityName} " +
$"at level: {nestingLevel} because the value is derived from the creation of the record in it's parent entity specified in the request.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
}
}
}
}