Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,26 +187,49 @@ 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="visitedEntity"> 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>? visitedEntity = null)
{
// Traverse the fields and add them to the path
foreach (FieldDefinitionNode field in fields)
{
// Create a tracker to keep track of visited entities to detect circular references
HashSet<string> trackerForField = new();
if (visitedEntity is not null)
{
trackerForField = visitedEntity;
}

string entityType = field.Type.NamedType().Name.Value;

// If the entity is not in the runtime config, skip it
if (!_runtimeConfig.Entities.ContainsKey(entityType))
{
continue;
}

// If the entity is already visited, then it is a circular reference
if (!trackerForField.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 +258,15 @@ private void ProcessSchema(IReadOnlyList<FieldDefinitionNode> fields,
});
}

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 +276,8 @@ private void ProcessSchema(IReadOnlyList<FieldDefinitionNode> fields,
schemaDocument: schemaDocument,
currentPath: isArrayType ? $"{alias}" : $"{currentPath}.{field.Name.Value}",
tableCounter: tableCounter,
previousEntity: isArrayType ? currentEntity : null);
parentEntity: isArrayType ? currentEntity : null,
visitedEntity: trackerForField);
}
}

Expand Down
78 changes: 78 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 = @"
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
}
";

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,42 @@ 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, the
/// '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) },
{ FileSystemRuntimeConfigLoader.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(System.Net.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