Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation to disallow multiple create on related entities backed by same database table #2189

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c5d12a0
Validation to disallow mutations on related entities backed by same d…
ayush3797 Apr 26, 2024
9978ca5
Merge branch 'main' into dev/agarwalayush/prohibitSelfReferencingRela…
ayush3797 Apr 26, 2024
f7ec86c
nit
ayush3797 Apr 26, 2024
2b82df7
Add test for self ref relationship
ayush3797 Apr 26, 2024
d4bfb35
adding test/method summary
ayush3797 Apr 26, 2024
5111bd5
Adding tests for My/Ms/PSql
ayush3797 Apr 26, 2024
ae33246
Update src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCr…
ayush3797 Apr 26, 2024
6bfd1d0
Update src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCr…
ayush3797 Apr 26, 2024
83f887a
Update src/Core/Resolvers/MultipleCreateOrderHelper.cs
ayush3797 May 1, 2024
fd1a043
Update src/Core/Resolvers/MultipleCreateOrderHelper.cs
ayush3797 May 1, 2024
897fe23
Update src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCr…
ayush3797 May 1, 2024
2fc5827
Update src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCr…
ayush3797 May 1, 2024
9b300d0
Update src/Core/Resolvers/MultipleCreateOrderHelper.cs
ayush3797 May 1, 2024
007ce79
Merge branch 'main' into dev/agarwalayush/prohibitSelfReferencingRela…
ayush3797 May 1, 2024
eaa4f52
checking type before downcasting
ayush3797 May 1, 2024
da0b65a
formatting fix
ayush3797 May 1, 2024
ac766fe
fixing expected error message
ayush3797 May 1, 2024
ec20de4
Merge branch 'main' into dev/agarwalayush/prohibitSelfReferencingRela…
ayush3797 May 2, 2024
08f54c1
Merge branch 'main' into dev/agarwalayush/prohibitSelfReferencingRela…
ayush3797 May 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions config-generators/mysql-commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ init --config "dab-config.MySql.json" --database-type mysql --connection-string
add Publisher --config "dab-config.MySql.json" --source publishers --permissions "anonymous:read"
add Stock --config "dab-config.MySql.json" --source stocks --permissions "anonymous:create,read,update,delete"
add Book --config "dab-config.MySql.json" --source books --permissions "anonymous:create,read,update,delete" --graphql "book:books"
add BookNF --config "dab-config.MySql.json" --source books --permissions "anonymous:*" --rest true --graphql "bookNF:booksNF"
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
add BookWebsitePlacement --config "dab-config.MySql.json" --source book_website_placements --permissions "anonymous:read"
add Author --config "dab-config.MySql.json" --source authors --permissions "anonymous:read"
add Review --config "dab-config.MySql.json" --source reviews --permissions "anonymous:create,read,update" --rest false --graphql "review:reviews"
Expand Down
27 changes: 25 additions & 2 deletions src/Core/Resolvers/MultipleCreateOrderHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ public class MultipleCreateOrderHelper
/// returns the referencing entity's name for the pair of (source, target) entities.
///
/// When visualized as a graphQL mutation request,
/// Source entity refers to the top level entity
/// Source entity refers to the top level entity in whose configuration the relationship is defined.
/// Target entity refers to the related entity.
///
/// This method handles the logic to determine the referencing entity for relationships from (source, target) with cardinalities:
/// 1. 1:N - Target entity is the referencing entity
/// 2. N:1 - Source entity is the referencing entity
/// 3. 1:1 - Determined based on foreign key constraint/request input data.
/// 4. M:N - None of the source/target entity is the referencing entity. Instead, linking table act as the referencing table.
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="context">GraphQL request context.</param>
/// <param name="relationshipName">Configured relationship name in the config file b/w source and target entity.</param>
Expand All @@ -40,6 +41,7 @@ public class MultipleCreateOrderHelper
/// <param name="columnDataInSourceBody">Column name/value for backing columns present in the request input for the source entity.</param>
/// <param name="targetNodeValue">Input GraphQL value for target node (could be an object or array).</param>
/// <param name="nestingLevel">Nesting level of the entity in the mutation request.</param>
/// <param name="isMNRelationship">true if the relationship is a Many-Many relationship.</param>
public static string GetReferencingEntityName(
IMiddlewareContext context,
string relationshipName,
Expand All @@ -48,7 +50,8 @@ public class MultipleCreateOrderHelper
ISqlMetadataProvider metadataProvider,
Dictionary<string, IValueNode?> columnDataInSourceBody,
IValueNode? targetNodeValue,
int nestingLevel)
int nestingLevel,
bool isMNRelationship = false)
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
{
if (!metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbObject) ||
!metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbObject))
Expand All @@ -61,6 +64,26 @@ public class MultipleCreateOrderHelper
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject;
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
DatabaseTable targetDbTable = (DatabaseTable)targetDbObject;
if (sourceDbTable.Equals(targetDbTable))
{
// DAB does not yet support multiple-create for self referencing relationships where both the source and
// target entities are backed by same database table.
throw new DataApiBuilderException(
message: $"Multiple-create for relationship: {relationshipName} at level: {nestingLevel} is not supported because " +
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
$"source entity: {sourceEntityName} and target entity: {targetEntityName} are backed by same database table.",
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
}

if (isMNRelationship)
{
// For M:N relationships,'neither the source nor the target entity act as the referenced entity.
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
// Instead, the linking table act as the referencing entity.
return string.Empty;
}

if (TryDetermineReferencingEntityBasedOnEntityRelationshipMetadata(
sourceEntityName: sourceEntityName,
targetEntityName: targetEntityName,
Expand Down
23 changes: 13 additions & 10 deletions src/Core/Services/MultipleMutationInputValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,7 @@ public MultipleMutationInputValidator(IMetadataProviderFactory sqlMetadataProvid
const string relationshipSourceIdentifier = "$";
string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity;
string? linkingObject = runtimeConfig.Entities![entityName].Relationships![relationshipName].LinkingObject;
if (!string.IsNullOrWhiteSpace(linkingObject))
{
// 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;
}
bool isMNRelationship = !string.IsNullOrWhiteSpace(linkingObject);

// Determine the referencing entity for the current relationship field input.
string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName(
Expand All @@ -364,7 +356,18 @@ public MultipleMutationInputValidator(IMetadataProviderFactory sqlMetadataProvid
metadataProvider: metadataProvider,
columnDataInSourceBody: backingColumnData,
targetNodeValue: relationshipFieldValue,
nestingLevel: nestingLevel);
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;
}
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved

// Determine the referenced entity.
string referencedEntityName = referencingEntityName.Equals(entityName) ? targetEntityName : entityName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,32 @@
}
}
},
{
BookNF: {
Source: {
Object: books,
Type: Table
},
GraphQL: {
Singular: bookNF,
Plural: booksNF,
Enabled: true
},
Rest: {
Enabled: true
},
Permissions: [
{
Role: anonymous,
Actions: [
{
Action: *
}
]
}
]
}
},
{
BookWebsitePlacement: {
Source: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Net;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.Tests.SqlTests;
Expand Down Expand Up @@ -374,6 +375,23 @@ public void ValidateReferencingEntityBasedOnEntityMetadata()

#endregion

#region Order determination test for relationships having source/target entities backed by same database table

/// <summary>
/// Test to validate that when multiple-create is executed for a relationship for which source and target entities are backed by the
/// same database table, we throw an appropriate exception because DAB currently does not support multiple-create for such relationships.
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
[TestMethod]
public void TestExceptionForSelfReferencingRelationships()
{
// Identical source and target entities backed by the same database table 'books'.
ValidateExceptionForSelfReferencingRelationship(sourceEntityName: "Book", targetEntityName: "Book");

// Different source and target entities backed by the same database table 'books'.
ValidateExceptionForSelfReferencingRelationship(sourceEntityName: "Book", targetEntityName: "BookNF");
}
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
#endregion

#region Helpers
private static void ValidateReferencingEntityForRelationship(
string sourceEntityName,
Expand All @@ -395,6 +413,35 @@ public void ValidateReferencingEntityBasedOnEntityMetadata()
nestingLevel: 1);
Assert.AreEqual(expectedReferencingEntityName, actualReferencingEntityName);
}

/// <summary>
/// Helper method to validate the exception when multiple-create is executed for a self-referencing relationship where source and target
/// entities are backed by the same database table.
/// </summary>
/// <param name="sourceEntityName">Name of the source entity.</param>
/// <param name="targetEntityName">NAme of the target entity.</param>
private static void ValidateExceptionForSelfReferencingRelationship(
string sourceEntityName,
string targetEntityName)
{
// Setup mock IMiddlewareContext.
IMiddlewareContext context = SetupMiddlewareContext();
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() => MultipleCreateOrderHelper.GetReferencingEntityName(
relationshipName: "testRelationship", // Don't need relationship name while testing determination of referencing entity using metadata.
context: context,
sourceEntityName: sourceEntityName,
targetEntityName: targetEntityName,
metadataProvider: _sqlMetadataProvider,
columnDataInSourceBody: new(),
targetNodeValue: null,
nestingLevel: 1));

// Assert that the exception is as expected.
Assert.AreEqual(HttpStatusCode.BadRequest, ex.StatusCode);
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.NotSupported, ex.SubStatusCode);
Assert.AreEqual($"Multiple-create for relationship: testRelationship at level: 1 is not supported because " +
$"source entity: {sourceEntityName} and target entity: {targetEntityName} are backed by same database table.", ex.Message);
}
ayush3797 marked this conversation as resolved.
Show resolved Hide resolved
#endregion

#region Setup
Expand Down
26 changes: 26 additions & 0 deletions src/Service.Tests/dab-config.MySql.json
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,32 @@
}
}
},
"BookNF": {
"source": {
"object": "books",
"type": "table"
},
"graphql": {
"enabled": true,
"type": {
"singular": "bookNF",
"plural": "booksNF"
}
},
"rest": {
"enabled": true
},
"permissions": [
{
"role": "anonymous",
"actions": [
{
"action": "*"
}
]
}
]
},
"BookWebsitePlacement": {
"source": {
"object": "book_website_placements",
Expand Down