Skip to content

Commit 196fed9

Browse files
authored
Cosmos DB: Adds circular reference check for entities in graphQL schema (#2192)
## Why make this change? Today, if graphQL schema has circular reference of entities, DAB won't be able to load the schema and die with stackoverflow exception when it tries to traverse the schema. With JSON data, it can get very complicated to handle all the scenarios with circular reference. ## What is this change? Adding a proper exception during the load, if DAB identifies that schema has circular reference. If schema is found with circular reference, DAB will throw below exception. **DAB Exception** ``` c# DataApiBuilderException( message: $"Circular reference detected in the schema for entity '{entityType}'.", statusCode: System.Net.HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); ``` **Schema with circular reference** ```gql type Character { id : ID, name : String, moons: [Moon], // Character has Moon Reference } type Planet @model(name:""Planet"") { id : ID!, name : String, character: Character } type Moon { id : ID, name : String, details : String, character: Character // Moon has Character Reference } ``` ## How was this tested? - [x] Integration Tests - [x] Unit Tests
1 parent db235d0 commit 196fed9

File tree

2 files changed

+113
-13
lines changed

2 files changed

+113
-13
lines changed

src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -187,26 +187,48 @@ private void ParseSchemaGraphQLFieldsForJoins()
187187
/// 4. Check if we get previous entity with join information, if yes append it to the current entity also
188188
/// 5. Recursively call this function, to process the schema
189189
/// </summary>
190-
/// <param name="fields"></param>
191-
/// <param name="schemaDocument"></param>
192-
/// <param name="currentPath"></param>
193-
/// <param name="previousEntity">indicates the parent entity for which we are processing the schema.</param>
194-
private void ProcessSchema(IReadOnlyList<FieldDefinitionNode> fields,
190+
/// <param name="fields">All the fields of an entity</param>
191+
/// <param name="schemaDocument">Schema Documents, useful to get fields information of an entity</param>
192+
/// <param name="currentPath">Generated path of an entity</param>
193+
/// <param name="tableCounter">Counter used to generate table alias</param>
194+
/// <param name="parentEntity">indicates the parent entity for which we are processing the schema.
195+
/// It is useful to get the JOIN statement information and create further new statements</param>
196+
/// <param name="visitedEntities"> Keeps a track of the path in an entity, to detect circular reference</param>
197+
/// <remarks>It detects the circular reference in the schema while processing the schema and throws <seealso cref="DataApiBuilderException"/> </remarks>
198+
private void ProcessSchema(
199+
IReadOnlyList<FieldDefinitionNode> fields,
195200
Dictionary<string, ObjectTypeDefinitionNode> schemaDocument,
196201
string currentPath,
197202
IncrementingInteger tableCounter,
198-
EntityDbPolicyCosmosModel? previousEntity = null)
203+
EntityDbPolicyCosmosModel? parentEntity = null,
204+
HashSet<string>? visitedEntities = null)
199205
{
200206
// Traverse the fields and add them to the path
201207
foreach (FieldDefinitionNode field in fields)
202208
{
203-
string entityType = field.Type.NamedType().Name.Value;
204-
// If the entity is not in the runtime config, skip it
205-
if (!_runtimeConfig.Entities.ContainsKey(entityType))
209+
// Create a tracker to keep track of visited entities to detect circular references
210+
HashSet<string> trackerForFields = new();
211+
if (visitedEntities is not null)
212+
{
213+
trackerForFields = visitedEntities;
214+
}
215+
216+
// If the entity is build-in type, do not go further to check circular reference
217+
if (GraphQLUtils.IsBuiltInType(field.Type))
206218
{
207219
continue;
208220
}
209221

222+
string entityType = field.Type.NamedType().Name.Value;
223+
// If the entity is already visited, then it is a circular reference
224+
if (!trackerForFields.Add(entityType))
225+
{
226+
throw new DataApiBuilderException(
227+
message: $"Circular reference detected in the provided GraphQL schema for entity '{entityType}'.",
228+
statusCode: System.Net.HttpStatusCode.InternalServerError,
229+
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
230+
}
231+
210232
string? alias = null;
211233
bool isArrayType = field.Type is ListTypeNode;
212234
if (isArrayType)
@@ -235,15 +257,15 @@ private void ProcessSchema(IReadOnlyList<FieldDefinitionNode> fields,
235257
});
236258
}
237259

238-
if (previousEntity is not null)
260+
if (parentEntity is not null)
239261
{
240262
if (string.IsNullOrEmpty(currentEntity.JoinStatement))
241263
{
242-
currentEntity.JoinStatement = previousEntity.JoinStatement;
264+
currentEntity.JoinStatement = parentEntity.JoinStatement;
243265
}
244266
else
245267
{
246-
currentEntity.JoinStatement = previousEntity.JoinStatement + " JOIN " + currentEntity.JoinStatement;
268+
currentEntity.JoinStatement = parentEntity.JoinStatement + " JOIN " + currentEntity.JoinStatement;
247269
}
248270
}
249271

@@ -253,7 +275,8 @@ private void ProcessSchema(IReadOnlyList<FieldDefinitionNode> fields,
253275
schemaDocument: schemaDocument,
254276
currentPath: isArrayType ? $"{alias}" : $"{currentPath}.{field.Name.Value}",
255277
tableCounter: tableCounter,
256-
previousEntity: isArrayType ? currentEntity : null);
278+
parentEntity: isArrayType ? currentEntity : null,
279+
visitedEntities: trackerForFields);
257280
}
258281
}
259282

src/Service.Tests/Configuration/ConfigurationTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,48 @@ public class ConfigurationTests
425425
}
426426
}";
427427

428+
internal const string GRAPHQL_SCHEMA_WITH_CYCLE_ARRAY = @"
429+
type Character {
430+
id : ID,
431+
name : String,
432+
moons: [Moon],
433+
}
434+
435+
type Planet @model(name:""Planet"") {
436+
id : ID!,
437+
name : String,
438+
character: Character
439+
}
440+
441+
type Moon {
442+
id : ID,
443+
name : String,
444+
details : String,
445+
character: Character
446+
}
447+
";
448+
449+
internal const string GRAPHQL_SCHEMA_WITH_CYCLE_OBJECT = @"
450+
type Character {
451+
id : ID,
452+
name : String,
453+
moons: Moon,
454+
}
455+
456+
type Planet @model(name:""Planet"") {
457+
id : ID!,
458+
name : String,
459+
character: Character
460+
}
461+
462+
type Moon {
463+
id : ID,
464+
name : String,
465+
details : String,
466+
character: Character
467+
}
468+
";
469+
428470
[TestCleanup]
429471
public void CleanupAfterEachTest()
430472
{
@@ -2938,6 +2980,41 @@ public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL()
29382980
}
29392981
}
29402982

2983+
/// <summary>
2984+
/// In CosmosDB NoSQL, we store data in the form of JSON. Practically, JSON can be very complex.
2985+
/// But DAB doesn't support JSON with circular references e.g if 'Character.Moon' is a valid JSON Path, then
2986+
/// 'Moon.Character' should not be there, DAB would throw an exception during the load itself.
2987+
/// </summary>
2988+
/// <exception cref="ApplicationException"></exception>
2989+
[TestMethod, TestCategory(TestCategory.COSMOSDBNOSQL)]
2990+
[DataRow(GRAPHQL_SCHEMA_WITH_CYCLE_OBJECT, DisplayName = "When Circular Reference is there with Object type (i.e. 'Moon' in 'Character' Entity")]
2991+
[DataRow(GRAPHQL_SCHEMA_WITH_CYCLE_ARRAY, DisplayName = "When Circular Reference is there with Array type (i.e. '[Moon]' in 'Character' Entity")]
2992+
public void ValidateGraphQLSchemaForCircularReference(string schema)
2993+
{
2994+
// Read the base config from the file system
2995+
TestHelper.SetupDatabaseEnvironment(TestCategory.COSMOSDBNOSQL);
2996+
FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader();
2997+
if (!baseLoader.TryLoadKnownConfig(out RuntimeConfig baseConfig))
2998+
{
2999+
throw new ApplicationException("Failed to load the default CosmosDB_NoSQL config and cannot continue with tests.");
3000+
}
3001+
3002+
// Setup a mock file system, and use that one with the loader/provider for the config
3003+
MockFileSystem fileSystem = new(new Dictionary<string, MockFileData>()
3004+
{
3005+
{ @"../schema.gql", new MockFileData(schema) },
3006+
{ DEFAULT_CONFIG_FILE_NAME, new MockFileData(baseConfig.ToJson()) }
3007+
});
3008+
FileSystemRuntimeConfigLoader loader = new(fileSystem);
3009+
RuntimeConfigProvider provider = new(loader);
3010+
3011+
DataApiBuilderException exception =
3012+
Assert.ThrowsException<DataApiBuilderException>(() => new CosmosSqlMetadataProvider(provider, fileSystem));
3013+
Assert.AreEqual("Circular reference detected in the provided GraphQL schema for entity 'Character'.", exception.Message);
3014+
Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode);
3015+
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, exception.SubStatusCode);
3016+
}
3017+
29413018
/// <summary>
29423019
/// When you query, DAB loads schema and check for defined entities in the config file which get load during DAB initialization, and
29433020
/// 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.

0 commit comments

Comments
 (0)