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

Cosmos DB: Adds circular reference check for entities in graphQL schema #2192

Merged
merged 12 commits into from
May 3, 2024
49 changes: 36 additions & 13 deletions src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,26 +187,48 @@ private void ParseSchemaGraphQLFieldsForJoins()
/// 4. Check if we get previous entity with join information, if yes append it to the current entity also
/// 5. Recursively call this function, to process the schema
/// </summary>
/// <param name="fields"></param>
/// <param name="schemaDocument"></param>
/// <param name="currentPath"></param>
/// <param name="previousEntity">indicates the parent entity for which we are processing the schema.</param>
private void ProcessSchema(IReadOnlyList<FieldDefinitionNode> fields,
/// <param name="fields">All the fields of an entity</param>
/// <param name="schemaDocument">Schema Documents, useful to get fields information of an entity</param>
/// <param name="currentPath">Generated path of an entity</param>
/// <param name="tableCounter">Counter used to generate table alias</param>
/// <param name="parentEntity">indicates the parent entity for which we are processing the schema.
/// It is useful to get the JOIN statement information and create further new statements</param>
/// <param name="visitedEntities"> Keeps a track of the path in an entity, to detect circular reference</param>
/// <remarks>It detects the circular reference in the schema while processing the schema and throws <seealso cref="DataApiBuilderException"/> </remarks>
private void ProcessSchema(
IReadOnlyList<FieldDefinitionNode> fields,
Dictionary<string, ObjectTypeDefinitionNode> schemaDocument,
string currentPath,
IncrementingInteger tableCounter,
EntityDbPolicyCosmosModel? previousEntity = null)
EntityDbPolicyCosmosModel? parentEntity = null,
HashSet<string>? visitedEntities = null)
{
// Traverse the fields and add them to the path
foreach (FieldDefinitionNode field in fields)
{
string entityType = field.Type.NamedType().Name.Value;
// If the entity is not in the runtime config, skip it
if (!_runtimeConfig.Entities.ContainsKey(entityType))
// Create a tracker to keep track of visited entities to detect circular references
HashSet<string> trackerForFields = new();
if (visitedEntities is not null)
{
trackerForFields = visitedEntities;
}

// If the entity is build-in type, do not go further to check circular reference
if (GraphQLUtils.IsBuiltInType(field.Type))
{
continue;
sourabh1007 marked this conversation as resolved.
Show resolved Hide resolved
}

string entityType = field.Type.NamedType().Name.Value;
// If the entity is already visited, then it is a circular reference
if (!trackerForFields.Add(entityType))
{
throw new DataApiBuilderException(
message: $"Circular reference detected in the provided GraphQL schema for entity '{entityType}'.",
statusCode: System.Net.HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
}

string? alias = null;
bool isArrayType = field.Type is ListTypeNode;
if (isArrayType)
Expand Down Expand Up @@ -235,15 +257,15 @@ private void ParseSchemaGraphQLFieldsForJoins()
});
}

if (previousEntity is not null)
if (parentEntity is not null)
{
if (string.IsNullOrEmpty(currentEntity.JoinStatement))
{
currentEntity.JoinStatement = previousEntity.JoinStatement;
currentEntity.JoinStatement = parentEntity.JoinStatement;
}
else
{
currentEntity.JoinStatement = previousEntity.JoinStatement + " JOIN " + currentEntity.JoinStatement;
currentEntity.JoinStatement = parentEntity.JoinStatement + " JOIN " + currentEntity.JoinStatement;
}
}

Expand All @@ -253,7 +275,8 @@ private void ParseSchemaGraphQLFieldsForJoins()
schemaDocument: schemaDocument,
currentPath: isArrayType ? $"{alias}" : $"{currentPath}.{field.Name.Value}",
tableCounter: tableCounter,
previousEntity: isArrayType ? currentEntity : null);
parentEntity: isArrayType ? currentEntity : null,
visitedEntities: trackerForFields);
}
}

Expand Down
77 changes: 77 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,48 @@ public class ConfigurationTests
}
}";

internal const string GRAPHQL_SCHEMA_WITH_CYCLE_ARRAY = @"
sourabh1007 marked this conversation as resolved.
Show resolved Hide resolved
type Character {
id : ID,
name : String,
moons: [Moon],
}

type Planet @model(name:""Planet"") {
id : ID!,
name : String,
character: Character
}

type Moon {
id : ID,
name : String,
details : String,
character: Character
}
";
sourabh1007 marked this conversation as resolved.
Show resolved Hide resolved

internal const string GRAPHQL_SCHEMA_WITH_CYCLE_OBJECT = @"
type Character {
id : ID,
name : String,
moons: Moon,
}

type Planet @model(name:""Planet"") {
id : ID!,
name : String,
character: Character
}

type Moon {
id : ID,
name : String,
details : String,
character: Character
}
";

[TestCleanup]
public void CleanupAfterEachTest()
{
Expand Down Expand Up @@ -2938,6 +2980,41 @@ public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL()
}
}

/// <summary>
/// In CosmosDB NoSQL, we store data in the form of JSON. Practically, JSON can be very complex.
/// But DAB doesn't support JSON with circular references e.g if 'Character.Moon' is a valid JSON Path, then
/// 'Moon.Character' should not be there, DAB would throw an exception during the load itself.
/// </summary>
/// <exception cref="ApplicationException"></exception>
[TestMethod, TestCategory(TestCategory.COSMOSDBNOSQL)]
[DataRow(GRAPHQL_SCHEMA_WITH_CYCLE_OBJECT, DisplayName = "When Circular Reference is there with Object type (i.e. 'Moon' in 'Character' Entity")]
[DataRow(GRAPHQL_SCHEMA_WITH_CYCLE_ARRAY, DisplayName = "When Circular Reference is there with Array type (i.e. '[Moon]' in 'Character' Entity")]
public void ValidateGraphQLSchemaForCircularReference(string schema)
{
// Read the base config from the file system
TestHelper.SetupDatabaseEnvironment(TestCategory.COSMOSDBNOSQL);
FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader();
if (!baseLoader.TryLoadKnownConfig(out RuntimeConfig baseConfig))
{
throw new ApplicationException("Failed to load the default CosmosDB_NoSQL config and cannot continue with tests.");
}

// Setup a mock file system, and use that one with the loader/provider for the config
MockFileSystem fileSystem = new(new Dictionary<string, MockFileData>()
{
{ @"../schema.gql", new MockFileData(schema) },
{ DEFAULT_CONFIG_FILE_NAME, new MockFileData(baseConfig.ToJson()) }
});
FileSystemRuntimeConfigLoader loader = new(fileSystem);
RuntimeConfigProvider provider = new(loader);

DataApiBuilderException exception =
Assert.ThrowsException<DataApiBuilderException>(() => new CosmosSqlMetadataProvider(provider, fileSystem));
Assert.AreEqual("Circular reference detected in the provided GraphQL schema for entity 'Character'.", exception.Message);
Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode);
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, exception.SubStatusCode);
}

/// <summary>
/// When you query, DAB loads schema and check for defined entities in the config file which get load during DAB initialization, and
/// it fails during this check if entity is not defined in the config file. In this test case, we are testing the error message is appropriate.
Expand Down