diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json
index 95172de225..4159308c18 100644
--- a/schemas/dab.draft.schema.json
+++ b/schemas/dab.draft.schema.json
@@ -175,6 +175,25 @@
"enabled": {
"type": "boolean",
"description": "Allow enabling/disabling GraphQL requests for all entities."
+ },
+ "nested-mutations": {
+ "type": "object",
+ "description": "Configuration properties for nested mutation operations",
+ "additionalProperties": false,
+ "properties": {
+ "create":{
+ "type": "object",
+ "description": "Options for nested create operations",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Allow enabling/disabling nested create operations for all entities.",
+ "default": false
+ }
+ }
+ }
+ }
}
}
},
diff --git a/src/Cli.Tests/ConfigGeneratorTests.cs b/src/Cli.Tests/ConfigGeneratorTests.cs
index 52c2019d39..6094189f93 100644
--- a/src/Cli.Tests/ConfigGeneratorTests.cs
+++ b/src/Cli.Tests/ConfigGeneratorTests.cs
@@ -162,7 +162,7 @@ public void TestSpecialCharactersInConnectionString()
""enabled"": true,
""path"": ""/An_"",
""allow-introspection"": true
- },
+ },
""host"": {
""cors"": {
""origins"": [],
diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs
index 143013e3dc..f15b744d36 100644
--- a/src/Cli.Tests/EndToEndTests.cs
+++ b/src/Cli.Tests/EndToEndTests.cs
@@ -131,6 +131,90 @@ public void TestInitializingRestAndGraphQLGlobalSettings()
Assert.IsTrue(runtimeConfig.Runtime.GraphQL?.Enabled);
}
+ ///
+ /// Test to validate the usage of --graphql.nested-create.enabled option of the init command for all database types.
+ ///
+ /// 1. Behavior for database types other than MsSQL:
+ /// - Irrespective of whether the --graphql.nested-create.enabled option is used or not, fields related to nested-create will NOT be written to the config file.
+ /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be null.
+ /// 2. Behavior for MsSQL database type:
+ ///
+ /// a. When --graphql.nested-create.enabled option is used
+ /// - In this case, the fields related to nested mutation and nested create operations will be written to the config file.
+ /// "nested-mutations": {
+ /// "create": {
+ /// "enabled": true/false
+ /// }
+ /// }
+ /// After deserializing such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be non-null and the value of the "enabled" field is expected to be the same as the value passed in the init command.
+ ///
+ /// b. When --graphql.nested-create.enabled option is not used
+ /// - In this case, fields related to nested mutation and nested create operations will NOT be written to the config file.
+ /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be null.
+ ///
+ ///
+ /// Value interpreted by the CLI for '--graphql.nested-create.enabled' option of the init command.
+ /// When not used, CLI interprets the value for the option as CliBool.None
+ /// When used with true/false, CLI interprets the value as CliBool.True/CliBool.False respectively.
+ ///
+ /// Expected value for the nested create enabled flag in the config file.
+ [DataTestMethod]
+ [DataRow(CliBool.True, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MsSql database type")]
+ [DataRow(CliBool.False, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MsSql database type")]
+ [DataRow(CliBool.None, "mssql", DatabaseType.MSSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MsSql database type")]
+ [DataRow(CliBool.True, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MySql database type")]
+ [DataRow(CliBool.False, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MySql database type")]
+ [DataRow(CliBool.None, "mysql", DatabaseType.MySQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MySql database type")]
+ [DataRow(CliBool.True, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for PostgreSql database type")]
+ [DataRow(CliBool.False, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for PostgreSql database type")]
+ [DataRow(CliBool.None, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for PostgreSql database type")]
+ [DataRow(CliBool.True, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for dwsql database type")]
+ [DataRow(CliBool.False, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for dwsql database type")]
+ [DataRow(CliBool.None, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for dwsql database type")]
+ [DataRow(CliBool.True, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for cosmosdb_nosql database type")]
+ [DataRow(CliBool.False, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for cosmosdb_nosql database type")]
+ [DataRow(CliBool.None, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for cosmosdb_nosql database type")]
+ public void TestEnablingNestedCreateOperation(CliBool isNestedCreateEnabled, string dbType, DatabaseType expectedDbType)
+ {
+ List args = new() { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--connection-string", SAMPLE_TEST_CONN_STRING, "--database-type", dbType };
+
+ if (string.Equals("cosmosdb_nosql", dbType, StringComparison.OrdinalIgnoreCase))
+ {
+ List cosmosNoSqlArgs = new() { "--cosmosdb_nosql-database",
+ "graphqldb", "--cosmosdb_nosql-container", "planet", "--graphql-schema", TEST_SCHEMA_FILE};
+ args.AddRange(cosmosNoSqlArgs);
+ }
+
+ if (isNestedCreateEnabled is not CliBool.None)
+ {
+ args.Add("--graphql.nested-create.enabled");
+ args.Add(isNestedCreateEnabled.ToString()!);
+ }
+
+ Program.Execute(args.ToArray(), _cliLogger!, _fileSystem!, _runtimeConfigLoader!);
+
+ Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(
+ TEST_RUNTIME_CONFIG_FILE,
+ out RuntimeConfig? runtimeConfig,
+ replaceEnvVar: true));
+
+ Assert.IsNotNull(runtimeConfig);
+ Assert.AreEqual(expectedDbType, runtimeConfig.DataSource.DatabaseType);
+ Assert.IsNotNull(runtimeConfig.Runtime);
+ Assert.IsNotNull(runtimeConfig.Runtime.GraphQL);
+ if (runtimeConfig.DataSource.DatabaseType is DatabaseType.MSSQL && isNestedCreateEnabled is not CliBool.None)
+ {
+ Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions);
+ Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions.NestedCreateOptions);
+ bool expectedValueForNestedCreateEnabled = isNestedCreateEnabled == CliBool.True;
+ Assert.AreEqual(expectedValueForNestedCreateEnabled, runtimeConfig.Runtime.GraphQL.NestedMutationOptions.NestedCreateOptions.Enabled);
+ }
+ else
+ {
+ Assert.IsNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions, message: "NestedMutationOptions is expected to be null because a) DB type is not MsSQL or b) Either --graphql.nested-create.enabled option was not used or no value was provided.");
+ }
+ }
+
///
/// Test to verify adding a new Entity.
///
diff --git a/src/Cli.Tests/InitTests.cs b/src/Cli.Tests/InitTests.cs
index 31cff258a7..bfd0a7a19c 100644
--- a/src/Cli.Tests/InitTests.cs
+++ b/src/Cli.Tests/InitTests.cs
@@ -409,6 +409,89 @@ public Task GraphQLPathWithoutStartingSlashWillHaveItAdded()
return ExecuteVerifyTest(options);
}
+ ///
+ /// Test to validate the contents of the config file generated when init command is used with --graphql.nested-create.enabled flag option for different database types.
+ ///
+ /// 1. For database types other than MsSQL:
+ /// - Irrespective of whether the --graphql.nested-create.enabled option is used or not, fields related to nested-create will NOT be written to the config file.
+ ///
+ /// 2. For MsSQL database type:
+ /// a. When --graphql.nested-create.enabled option is used
+ /// - In this case, the fields related to nested mutation and nested create operations will be written to the config file.
+ /// "nested-mutations": {
+ /// "create": {
+ /// "enabled": true/false
+ /// }
+ /// }
+ ///
+ /// b. When --graphql.nested-create.enabled option is not used
+ /// - In this case, fields related to nested mutation and nested create operations will NOT be written to the config file.
+ ///
+ ///
+ [DataTestMethod]
+ [DataRow(DatabaseType.MSSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MsSQL database type")]
+ [DataRow(DatabaseType.MSSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MsSQL database type")]
+ [DataRow(DatabaseType.MSSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MsSQL database type")]
+ [DataRow(DatabaseType.PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for PostgreSQL database type")]
+ [DataRow(DatabaseType.PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for PostgreSQL database type")]
+ [DataRow(DatabaseType.PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for PostgreSQL database type")]
+ [DataRow(DatabaseType.MySQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MySQL database type")]
+ [DataRow(DatabaseType.MySQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MySQL database type")]
+ [DataRow(DatabaseType.MySQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MySQL database type")]
+ [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for CosmosDB_NoSQL database type")]
+ [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for CosmosDB_NoSQL database type")]
+ [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for CosmosDB_NoSQL database type")]
+ [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for CosmosDB_PostgreSQL database type")]
+ [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for CosmosDB_PostgreSQL database type")]
+ [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for CosmosDB_PostgreSQL database type")]
+ [DataRow(DatabaseType.DWSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for DWSQL database type")]
+ [DataRow(DatabaseType.DWSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for DWSQL database type")]
+ [DataRow(DatabaseType.DWSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for DWSQL database type")]
+ public Task VerifyCorrectConfigGenerationWithNestedMutationOptions(DatabaseType databaseType, CliBool isNestedCreateEnabled)
+ {
+ InitOptions options;
+
+ if (databaseType is DatabaseType.CosmosDB_NoSQL)
+ {
+ // A schema file is added since its mandatory for CosmosDB_NoSQL
+ ((MockFileSystem)_fileSystem!).AddFile(TEST_SCHEMA_FILE, new MockFileData(""));
+
+ options = new(
+ databaseType: databaseType,
+ connectionString: "testconnectionstring",
+ cosmosNoSqlDatabase: "testdb",
+ cosmosNoSqlContainer: "testcontainer",
+ graphQLSchemaPath: TEST_SCHEMA_FILE,
+ setSessionContext: true,
+ hostMode: HostMode.Development,
+ corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" },
+ authenticationProvider: EasyAuthType.StaticWebApps.ToString(),
+ restPath: "rest-api",
+ config: TEST_RUNTIME_CONFIG_FILE,
+ nestedCreateOperationEnabled: isNestedCreateEnabled);
+ }
+ else
+ {
+ options = new(
+ databaseType: databaseType,
+ connectionString: "testconnectionstring",
+ cosmosNoSqlDatabase: null,
+ cosmosNoSqlContainer: null,
+ graphQLSchemaPath: null,
+ setSessionContext: true,
+ hostMode: HostMode.Development,
+ corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" },
+ authenticationProvider: EasyAuthType.StaticWebApps.ToString(),
+ restPath: "rest-api",
+ config: TEST_RUNTIME_CONFIG_FILE,
+ nestedCreateOperationEnabled: isNestedCreateEnabled);
+ }
+
+ VerifySettings verifySettings = new();
+ verifySettings.UseHashedParameters(databaseType, isNestedCreateEnabled);
+ return ExecuteVerifyTest(options, verifySettings);
+ }
+
private Task ExecuteVerifyTest(InitOptions options, VerifySettings? settings = null)
{
Assert.IsTrue(TryCreateRuntimeConfig(options, _runtimeConfigLoader!, _fileSystem!, out RuntimeConfig? runtimeConfig));
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt
new file mode 100644
index 0000000000..9740a85a77
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt
@@ -0,0 +1,34 @@
+{
+ DataSource: {
+ Options: {
+ container: testcontainer,
+ database: testdb,
+ schema: test-schema.gql
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: false,
+ Path: /api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt
new file mode 100644
index 0000000000..da7937d1d9
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt
@@ -0,0 +1,33 @@
+{
+ DataSource: {
+ DatabaseType: MSSQL,
+ Options: {
+ set-session-context: true
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt
new file mode 100644
index 0000000000..078169b766
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt
@@ -0,0 +1,38 @@
+{
+ DataSource: {
+ DatabaseType: MSSQL,
+ Options: {
+ set-session-context: true
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true,
+ NestedMutationOptions: {
+ NestedCreateOptions: {
+ Enabled: true
+ }
+ }
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt
new file mode 100644
index 0000000000..9740a85a77
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt
@@ -0,0 +1,34 @@
+{
+ DataSource: {
+ Options: {
+ container: testcontainer,
+ database: testdb,
+ schema: test-schema.gql
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: false,
+ Path: /api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt
new file mode 100644
index 0000000000..a43e68277c
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: MySQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt
new file mode 100644
index 0000000000..3285438ab7
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: PostgreSQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt
new file mode 100644
index 0000000000..cbaaa45754
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt
@@ -0,0 +1,33 @@
+{
+ DataSource: {
+ DatabaseType: DWSQL,
+ Options: {
+ set-session-context: true
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt
new file mode 100644
index 0000000000..794686467c
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt
@@ -0,0 +1,35 @@
+{
+ DataSource: {
+ DatabaseType: CosmosDB_PostgreSQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true,
+ NestedMutationOptions: {
+ NestedCreateOptions: {
+ Enabled: false
+ }
+ }
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt
new file mode 100644
index 0000000000..a43e68277c
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: MySQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt
new file mode 100644
index 0000000000..9740a85a77
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt
@@ -0,0 +1,34 @@
+{
+ DataSource: {
+ Options: {
+ container: testcontainer,
+ database: testdb,
+ schema: test-schema.gql
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: false,
+ Path: /api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt
new file mode 100644
index 0000000000..a43e68277c
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: MySQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt
new file mode 100644
index 0000000000..3285438ab7
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: PostgreSQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt
new file mode 100644
index 0000000000..673c21dae4
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: CosmosDB_PostgreSQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt
new file mode 100644
index 0000000000..cbaaa45754
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt
@@ -0,0 +1,33 @@
+{
+ DataSource: {
+ DatabaseType: DWSQL,
+ Options: {
+ set-session-context: true
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt
new file mode 100644
index 0000000000..cbaaa45754
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt
@@ -0,0 +1,33 @@
+{
+ DataSource: {
+ DatabaseType: DWSQL,
+ Options: {
+ set-session-context: true
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt
new file mode 100644
index 0000000000..65cf6b8748
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt
@@ -0,0 +1,38 @@
+{
+ DataSource: {
+ DatabaseType: MSSQL,
+ Options: {
+ set-session-context: true
+ }
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true,
+ NestedMutationOptions: {
+ NestedCreateOptions: {
+ Enabled: false
+ }
+ }
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt
new file mode 100644
index 0000000000..3285438ab7
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: PostgreSQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt
new file mode 100644
index 0000000000..673c21dae4
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: CosmosDB_PostgreSQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt
new file mode 100644
index 0000000000..673c21dae4
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt
@@ -0,0 +1,30 @@
+{
+ DataSource: {
+ DatabaseType: CosmosDB_PostgreSQL
+ },
+ Runtime: {
+ Rest: {
+ Enabled: true,
+ Path: /rest-api,
+ RequestBodyStrict: true
+ },
+ GraphQL: {
+ Enabled: true,
+ Path: /graphql,
+ AllowIntrospection: true
+ },
+ Host: {
+ Cors: {
+ Origins: [
+ http://localhost:3000,
+ http://nolocalhost:80
+ ],
+ AllowCredentials: false
+ },
+ Authentication: {
+ Provider: StaticWebApps
+ }
+ }
+ },
+ Entities: []
+}
\ No newline at end of file
diff --git a/src/Cli/Commands/InitOptions.cs b/src/Cli/Commands/InitOptions.cs
index 1d7ec4bf59..a3d49d6bb1 100644
--- a/src/Cli/Commands/InitOptions.cs
+++ b/src/Cli/Commands/InitOptions.cs
@@ -37,6 +37,7 @@ public InitOptions(
CliBool restEnabled = CliBool.None,
CliBool graphqlEnabled = CliBool.None,
CliBool restRequestBodyStrict = CliBool.None,
+ CliBool nestedCreateOperationEnabled = CliBool.None,
string? config = null)
: base(config)
{
@@ -59,6 +60,7 @@ public InitOptions(
RestEnabled = restEnabled;
GraphQLEnabled = graphqlEnabled;
RestRequestBodyStrict = restRequestBodyStrict;
+ NestedCreateOperationEnabled = nestedCreateOperationEnabled;
}
[Option("database-type", Required = true, HelpText = "Type of database to connect. Supported values: mssql, cosmosdb_nosql, cosmosdb_postgresql, mysql, postgresql, dwsql")]
@@ -120,6 +122,9 @@ public InitOptions(
[Option("rest.request-body-strict", Required = false, HelpText = "(Default: true) Allow extraneous fields in the request body for REST.")]
public CliBool RestRequestBodyStrict { get; }
+ [Option("graphql.nested-create.enabled", Required = false, HelpText = "(Default: false) Enables nested create operation for GraphQL. Supported values: true, false.")]
+ public CliBool NestedCreateOperationEnabled { get; }
+
public void Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 0c22599fc1..94ae62ab0d 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -113,6 +113,27 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime
return false;
}
+ bool isNestedCreateEnabledForGraphQL;
+
+ // Nested mutation operations are applicable only for MSSQL database. When the option --graphql.nested-create.enabled is specified for other database types,
+ // a warning is logged.
+ // When nested mutation operations are extended for other database types, this option should be honored.
+ // Tracked by issue #2001: https://github.com/Azure/data-api-builder/issues/2001.
+ if (dbType is not DatabaseType.MSSQL && options.NestedCreateOperationEnabled is not CliBool.None)
+ {
+ _logger.LogWarning($"The option --graphql.nested-create.enabled is not supported for the {dbType.ToString()} database type and will not be honored.");
+ }
+
+ NestedMutationOptions? nestedMutationOptions = null;
+
+ // Nested mutation operations are applicable only for MSSQL database. When the option --graphql.nested-create.enabled is specified for other database types,
+ // it is not honored.
+ if (dbType is DatabaseType.MSSQL && options.NestedCreateOperationEnabled is not CliBool.None)
+ {
+ isNestedCreateEnabledForGraphQL = IsNestedCreateOperationEnabled(options.NestedCreateOperationEnabled);
+ nestedMutationOptions = new(nestedCreateOptions: new NestedCreateOptions(enabled: isNestedCreateEnabledForGraphQL));
+ }
+
switch (dbType)
{
case DatabaseType.CosmosDB_NoSQL:
@@ -232,7 +253,7 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime
DataSource: dataSource,
Runtime: new(
Rest: new(restEnabled, restPath ?? RestRuntimeOptions.DEFAULT_PATH, options.RestRequestBodyStrict is CliBool.False ? false : true),
- GraphQL: new(graphQLEnabled, graphQLPath),
+ GraphQL: new(Enabled: graphQLEnabled, Path: graphQLPath, NestedMutationOptions: nestedMutationOptions),
Host: new(
Cors: new(options.CorsOrigin?.ToArray() ?? Array.Empty()),
Authentication: new(
@@ -285,6 +306,16 @@ private static bool TryDetermineIfApiIsEnabled(bool apiDisabledOptionValue, CliB
return true;
}
+ ///
+ /// Helper method to determine if the nested create operation is enabled or not based on the inputs from dab init command.
+ ///
+ /// Input value for --graphql.nested-create.enabled option of the init command
+ /// True/False
+ private static bool IsNestedCreateOperationEnabled(CliBool nestedCreateEnabledOptionValue)
+ {
+ return nestedCreateEnabledOptionValue is CliBool.True;
+ }
+
///
/// This method will add a new Entity with the given REST and GraphQL endpoints, source, and permissions.
/// It also supports fields that needs to be included or excluded for a given role and operation.
diff --git a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs
index 7bca48106a..0101cbba87 100644
--- a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs
+++ b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs
@@ -9,6 +9,10 @@ namespace Azure.DataApiBuilder.Config.Converters;
internal class GraphQLRuntimeOptionsConverterFactory : JsonConverterFactory
{
+ // Determines whether to replace environment variable with its
+ // value or not while deserializing.
+ private bool _replaceEnvVar;
+
///
public override bool CanConvert(Type typeToConvert)
{
@@ -18,11 +22,27 @@ public override bool CanConvert(Type typeToConvert)
///
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
- return new GraphQLRuntimeOptionsConverter();
+ return new GraphQLRuntimeOptionsConverter(_replaceEnvVar);
+ }
+
+ internal GraphQLRuntimeOptionsConverterFactory(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
}
private class GraphQLRuntimeOptionsConverter : JsonConverter
{
+ // Determines whether to replace environment variable with its
+ // value or not while deserializing.
+ private bool _replaceEnvVar;
+
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
+ internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
+ }
+
public override GraphQLRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.Null)
@@ -35,11 +55,85 @@ private class GraphQLRuntimeOptionsConverter : JsonConverter c is GraphQLRuntimeOptionsConverterFactory));
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ // Initialize with Nested Mutation operations disabled by default
+ GraphQLRuntimeOptions graphQLRuntimeOptions = new();
+ NestedMutationOptionsConverter nestedMutationOptionsConverter = options.GetConverter(typeof(NestedMutationOptions)) as NestedMutationOptionsConverter ??
+ throw new JsonException("Failed to get nested mutation options converter");
+
+ while (reader.Read())
+ {
+
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ string? propertyName = reader.GetString();
+
+ if (propertyName is null)
+ {
+ throw new JsonException("Invalid property : null");
+ }
+
+ reader.Read();
+ switch (propertyName)
+ {
+ case "enabled":
+ if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False)
+ {
+ graphQLRuntimeOptions = graphQLRuntimeOptions with { Enabled = reader.GetBoolean() };
+ }
+ else
+ {
+ throw new JsonException($"Unsupported value entered for the property 'enabled': {reader.TokenType}");
+ }
+
+ break;
- return JsonSerializer.Deserialize(ref reader, innerOptions);
+ case "allow-introspection":
+ if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False)
+ {
+ graphQLRuntimeOptions = graphQLRuntimeOptions with { AllowIntrospection = reader.GetBoolean() };
+ }
+ else
+ {
+ throw new JsonException($"Unexpected type of value entered for allow-introspection: {reader.TokenType}");
+ }
+
+ break;
+ case "path":
+ if (reader.TokenType is JsonTokenType.String)
+ {
+ string? path = reader.DeserializeString(_replaceEnvVar);
+ if (path is null)
+ {
+ path = "/graphql";
+ }
+
+ graphQLRuntimeOptions = graphQLRuntimeOptions with { Path = path };
+ }
+ else
+ {
+ throw new JsonException($"Unexpected type of value entered for path: {reader.TokenType}");
+ }
+
+ break;
+
+ case "nested-mutations":
+ graphQLRuntimeOptions = graphQLRuntimeOptions with { NestedMutationOptions = nestedMutationOptionsConverter.Read(ref reader, typeToConvert, options) };
+ break;
+
+ default:
+ throw new JsonException($"Unexpected property {propertyName}");
+ }
+ }
+
+ return graphQLRuntimeOptions;
+ }
+
+ throw new JsonException("Failed to read the GraphQL Runtime Options");
}
public override void Write(Utf8JsonWriter writer, GraphQLRuntimeOptions value, JsonSerializerOptions options)
@@ -48,6 +142,16 @@ public override void Write(Utf8JsonWriter writer, GraphQLRuntimeOptions value, J
writer.WriteBoolean("enabled", value.Enabled);
writer.WriteString("path", value.Path);
writer.WriteBoolean("allow-introspection", value.AllowIntrospection);
+
+ if (value.NestedMutationOptions is not null)
+ {
+
+ NestedMutationOptionsConverter nestedMutationOptionsConverter = options.GetConverter(typeof(NestedMutationOptions)) as NestedMutationOptionsConverter ??
+ throw new JsonException("Failed to get nested mutation options converter");
+
+ nestedMutationOptionsConverter.Write(writer, value.NestedMutationOptions, options);
+ }
+
writer.WriteEndObject();
}
}
diff --git a/src/Config/Converters/NestedCreateOptionsConverter.cs b/src/Config/Converters/NestedCreateOptionsConverter.cs
new file mode 100644
index 0000000000..7e495ef303
--- /dev/null
+++ b/src/Config/Converters/NestedCreateOptionsConverter.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Azure.DataApiBuilder.Config.ObjectModel;
+
+namespace Azure.DataApiBuilder.Config.Converters
+{
+ ///
+ /// Converter for the nested create operation options.
+ ///
+ internal class NestedCreateOptionsConverter : JsonConverter
+ {
+ ///
+ public override NestedCreateOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType is JsonTokenType.StartObject)
+ {
+ NestedCreateOptions? nestedCreateOptions = null;
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ string? propertyName = reader.GetString();
+
+ if (propertyName is null)
+ {
+ throw new JsonException("Invalid property : null");
+ }
+
+ switch (propertyName)
+ {
+ case "enabled":
+ reader.Read();
+ if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False)
+ {
+ nestedCreateOptions = new(reader.GetBoolean());
+ }
+
+ break;
+ default:
+ throw new JsonException($"Unexpected property {propertyName}");
+ }
+ }
+
+ return nestedCreateOptions;
+ }
+
+ throw new JsonException("Failed to read the GraphQL Nested Create options");
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, NestedCreateOptions? value, JsonSerializerOptions options)
+ {
+ // If the value is null, it is not written to the config file.
+ if (value is null)
+ {
+ return;
+ }
+
+ writer.WritePropertyName("create");
+
+ writer.WriteStartObject();
+ writer.WritePropertyName("enabled");
+ writer.WriteBooleanValue(value.Enabled);
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/src/Config/Converters/NestedMutationOptionsConverter.cs b/src/Config/Converters/NestedMutationOptionsConverter.cs
new file mode 100644
index 0000000000..f121e070dd
--- /dev/null
+++ b/src/Config/Converters/NestedMutationOptionsConverter.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Azure.DataApiBuilder.Config.ObjectModel;
+
+namespace Azure.DataApiBuilder.Config.Converters
+{
+ ///
+ /// Converter for the nested mutation options.
+ ///
+ internal class NestedMutationOptionsConverter : JsonConverter
+ {
+
+ private readonly NestedCreateOptionsConverter _nestedCreateOptionsConverter;
+
+ public NestedMutationOptionsConverter(JsonSerializerOptions options)
+ {
+ _nestedCreateOptionsConverter = options.GetConverter(typeof(NestedCreateOptions)) as NestedCreateOptionsConverter ??
+ throw new JsonException("Failed to get nested create options converter");
+ }
+
+ ///
+ public override NestedMutationOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType is JsonTokenType.StartObject)
+ {
+ NestedMutationOptions? nestedMutationOptions = null;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ string? propertyName = reader.GetString();
+ switch (propertyName)
+ {
+ case "create":
+ reader.Read();
+ NestedCreateOptions? nestedCreateOptions = _nestedCreateOptionsConverter.Read(ref reader, typeToConvert, options);
+ if (nestedCreateOptions is not null)
+ {
+ nestedMutationOptions = new(nestedCreateOptions);
+ }
+
+ break;
+
+ default:
+ throw new JsonException($"Unexpected property {propertyName}");
+ }
+ }
+
+ return nestedMutationOptions;
+ }
+
+ throw new JsonException("Failed to read the GraphQL Nested Mutation options");
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, NestedMutationOptions? value, JsonSerializerOptions options)
+ {
+ // If the nested mutation options is null, it is not written to the config file.
+ if (value is null)
+ {
+ return;
+ }
+
+ writer.WritePropertyName("nested-mutations");
+
+ writer.WriteStartObject();
+
+ if (value.NestedCreateOptions is not null)
+ {
+ _nestedCreateOptionsConverter.Write(writer, value.NestedCreateOptions, options);
+ }
+
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/src/Config/ObjectModel/GraphQLRuntimeOptions.cs b/src/Config/ObjectModel/GraphQLRuntimeOptions.cs
index 9969835cb2..9033d269e6 100644
--- a/src/Config/ObjectModel/GraphQLRuntimeOptions.cs
+++ b/src/Config/ObjectModel/GraphQLRuntimeOptions.cs
@@ -3,7 +3,10 @@
namespace Azure.DataApiBuilder.Config.ObjectModel;
-public record GraphQLRuntimeOptions(bool Enabled = true, string Path = GraphQLRuntimeOptions.DEFAULT_PATH, bool AllowIntrospection = true)
+public record GraphQLRuntimeOptions(bool Enabled = true,
+ string Path = GraphQLRuntimeOptions.DEFAULT_PATH,
+ bool AllowIntrospection = true,
+ NestedMutationOptions? NestedMutationOptions = null)
{
public const string DEFAULT_PATH = "/graphql";
}
diff --git a/src/Config/ObjectModel/NestedCreateOptions.cs b/src/Config/ObjectModel/NestedCreateOptions.cs
new file mode 100644
index 0000000000..8439646766
--- /dev/null
+++ b/src/Config/ObjectModel/NestedCreateOptions.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+namespace Azure.DataApiBuilder.Config.ObjectModel;
+
+///
+/// Options for nested create operations.
+///
+/// Indicates whether nested create operation is enabled.
+public class NestedCreateOptions
+{
+ ///
+ /// Indicates whether nested create operation is enabled.
+ ///
+ public bool Enabled;
+
+ public NestedCreateOptions(bool enabled)
+ {
+ Enabled = enabled;
+ }
+};
+
diff --git a/src/Config/ObjectModel/NestedMutationOptions.cs b/src/Config/ObjectModel/NestedMutationOptions.cs
new file mode 100644
index 0000000000..0cf6c05e3e
--- /dev/null
+++ b/src/Config/ObjectModel/NestedMutationOptions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.DataApiBuilder.Config.ObjectModel;
+
+///
+/// Class that holds the options for all nested mutation operations.
+///
+/// Options for nested create operation.
+public class NestedMutationOptions
+{
+ // Options for nested create operation.
+ public NestedCreateOptions? NestedCreateOptions;
+
+ public NestedMutationOptions(NestedCreateOptions? nestedCreateOptions = null)
+ {
+ NestedCreateOptions = nestedCreateOptions;
+ }
+
+ ///
+ /// Helper function that checks if nested create operation is enabled.
+ ///
+ /// True/False depending on whether nested create operation is enabled/disabled.
+ public bool IsNestedCreateOperationEnabled()
+ {
+ return NestedCreateOptions is not null && NestedCreateOptions.Enabled;
+ }
+
+}
diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs
index 0d64f35bdd..a597d94f8d 100644
--- a/src/Config/RuntimeConfigLoader.cs
+++ b/src/Config/RuntimeConfigLoader.cs
@@ -164,13 +164,15 @@ public static JsonSerializerOptions GetSerializationOptions(
};
options.Converters.Add(new EnumMemberJsonEnumConverterFactory());
options.Converters.Add(new RestRuntimeOptionsConverterFactory());
- options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory());
+ options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replaceEnvVar));
options.Converters.Add(new EntitySourceConverterFactory(replaceEnvVar));
options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replaceEnvVar));
options.Converters.Add(new EntityRestOptionsConverterFactory(replaceEnvVar));
options.Converters.Add(new EntityActionConverterFactory());
options.Converters.Add(new DataSourceFilesConverter());
options.Converters.Add(new EntityCacheOptionsConverterFactory());
+ options.Converters.Add(new NestedCreateOptionsConverter());
+ options.Converters.Add(new NestedMutationOptionsConverter(options));
options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar));
if (replaceEnvVar)
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index fc895e5cca..48db66f758 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -70,6 +70,43 @@ public class ConfigurationTests
private const int RETRY_COUNT = 5;
private const int RETRY_WAIT_SECONDS = 1;
+ ///
+ ///
+ ///
+ public const string BOOK_ENTITY_JSON = @"
+ {
+ ""entities"": {
+ ""Book"": {
+ ""source"": {
+ ""object"": ""books"",
+ ""type"": ""table""
+ },
+ ""graphql"": {
+ ""enabled"": true,
+ ""type"": {
+ ""singular"": ""book"",
+ ""plural"": ""books""
+ }
+ },
+ ""rest"":{
+ ""enabled"": true
+ },
+ ""permissions"": [
+ {
+ ""role"": ""anonymous"",
+ ""actions"": [
+ {
+ ""action"": ""read""
+ }
+ ]
+ }
+ ],
+ ""mappings"": null,
+ ""relationships"": null
+ }
+ }
+ }";
+
///
/// A valid REST API request body with correct parameter types for all the fields.
///
@@ -1581,6 +1618,119 @@ public async Task TestSPRestDefaultsForManuallyConstructedConfigs(
}
}
+ ///
+ /// Validates that deserialization of config file is successful for the following scenarios:
+ /// 1. Nested Mutations section is null
+ /// {
+ /// "nested-mutations": null
+ /// }
+ ///
+ /// 2. Nested Mutations section is empty.
+ /// {
+ /// "nested-mutations": {}
+ /// }
+ ///
+ /// 3. Create field within Nested Mutation section is null.
+ /// {
+ /// "nested-mutations": {
+ /// "create": null
+ /// }
+ /// }
+ ///
+ /// 4. Create field within Nested Mutation section is empty.
+ /// {
+ /// "nested-mutations": {
+ /// "create": {}
+ /// }
+ /// }
+ ///
+ /// For all the above mentioned scenarios, the expected value for NestedMutationOptions field is null.
+ ///
+ /// Base Config Json string.
+ [DataTestMethod]
+ [DataRow(TestHelper.BASE_CONFIG_NULL_NESTED_MUTATIONS_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when nested mutation section is null")]
+ [DataRow(TestHelper.BASE_CONFIG_EMPTY_NESTED_MUTATIONS_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when nested mutation section is empty")]
+ [DataRow(TestHelper.BASE_CONFIG_NULL_NESTED_CREATE_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when create field within nested mutation section is null")]
+ [DataRow(TestHelper.BASE_CONFIG_EMPTY_NESTED_CREATE_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when create field within nested mutation section is empty")]
+ public void ValidateDeserializationOfConfigWithNullOrEmptyInvalidNestedMutationSection(string baseConfig)
+ {
+ string configJson = TestHelper.AddPropertiesToJson(baseConfig, BOOK_ENTITY_JSON);
+ Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig));
+ Assert.IsNotNull(deserializedConfig.Runtime);
+ Assert.IsNotNull(deserializedConfig.Runtime.GraphQL);
+ Assert.IsNull(deserializedConfig.Runtime.GraphQL.NestedMutationOptions);
+ }
+
+ ///
+ /// Sanity check to validate that DAB engine starts successfully when used with a config file without the nested
+ /// mutations feature flag section.
+ /// The runtime graphql section of the config file used looks like this:
+ ///
+ /// "graphql": {
+ /// "path": "/graphql",
+ /// "allow-introspection": true
+ /// }
+ ///
+ /// Without the nested mutations feature flag section, DAB engine should be able to
+ /// 1. Successfully deserialize the config file without nested mutation section.
+ /// 2. Process REST and GraphQL API requests.
+ ///
+ ///
+ [TestMethod]
+ [TestCategory(TestCategory.MSSQL)]
+ public async Task SanityTestForRestAndGQLRequestsWithoutNestedMutationFeatureFlagSection()
+ {
+ // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the
+ // configuration file (instead of using CLI).
+ string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, BOOK_ENTITY_JSON);
+ Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)));
+ string configFileName = "custom-config.json";
+ File.WriteAllText(configFileName, deserializedConfig.ToJson());
+ string[] args = new[]
+ {
+ $"--ConfigFileName={configFileName}"
+ };
+
+ using (TestServer server = new(Program.CreateWebHostBuilder(args)))
+ using (HttpClient client = server.CreateClient())
+ {
+ try
+ {
+
+ // Perform a REST GET API request to validate that REST GET API requests are executed correctly.
+ HttpRequestMessage restRequest = new(HttpMethod.Get, "api/Book");
+ HttpResponseMessage restResponse = await client.SendAsync(restRequest);
+ Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode);
+
+ // Perform a GraphQL API request to validate that DAB engine executes GraphQL requests successfully.
+ string query = @"{
+ book_by_pk(id: 1) {
+ id,
+ title,
+ publisher_id
+ }
+ }";
+
+ object payload = new { query };
+
+ HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql")
+ {
+ Content = JsonContent.Create(payload)
+ };
+
+ HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest);
+ Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode);
+ Assert.IsNotNull(graphQLResponse.Content);
+ string body = await graphQLResponse.Content.ReadAsStringAsync();
+ Assert.IsFalse(body.Contains("errors"));
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Unexpected exception : {ex}");
+ }
+ }
+ }
+
///
/// Test to validate that when an entity which will return a paginated response is queried, and a custom runtime base route is configured in the runtime configuration,
/// then the generated nextLink in the response would contain the rest base-route just before the rest path. For the subsequent query, the rest base-route will be trimmed
diff --git a/src/Service.Tests/Multidab-config.MsSql.json b/src/Service.Tests/Multidab-config.MsSql.json
index 00aaac2ed2..9f05161a83 100644
--- a/src/Service.Tests/Multidab-config.MsSql.json
+++ b/src/Service.Tests/Multidab-config.MsSql.json
@@ -15,7 +15,12 @@
"graphql": {
"enabled": true,
"path": "/graphql",
- "allow-introspection": true
+ "allow-introspection": true,
+ "nested-mutations": {
+ "create": {
+ "enabled": true
+ }
+ }
},
"host": {
"cors": {
diff --git a/src/Service.Tests/TestHelper.cs b/src/Service.Tests/TestHelper.cs
index b1a1137b62..466ca311ef 100644
--- a/src/Service.Tests/TestHelper.cs
+++ b/src/Service.Tests/TestHelper.cs
@@ -194,6 +194,95 @@ public static RuntimeConfig AddMissingEntitiesToConfig(RuntimeConfig config, str
""entities"": {}" +
"}";
+ ///
+ /// An empty entities section of the config file. This is used in constructing config json strings utilized for testing.
+ ///
+ public const string EMPTY_ENTITIES_CONFIG_JSON =
+ @"
+ ""entities"": {}
+ ";
+
+ ///
+ /// A json string with Runtime Rest and GraphQL options. This is used in constructing config json strings utilized for testing.
+ ///
+ public const string RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON =
+ "{" +
+ SAMPLE_SCHEMA_DATA_SOURCE + "," +
+ @"
+ ""runtime"": {
+ ""rest"": {
+ ""path"": ""/api""
+ },
+ ""graphql"": {
+ ""path"": ""/graphql"",
+ ""allow-introspection"": true,";
+
+ ///
+ /// A json string with host and empty entity options. This is used in constructing config json strings utilized for testing.
+ ///
+ public const string HOST_AND_ENTITY_OPTIONS_CONFIG_JSON =
+ @"
+ ""host"": {
+ ""mode"": ""development"",
+ ""cors"": {
+ ""origins"": [""http://localhost:5000""],
+ ""allow-credentials"": false
+ },
+ ""authentication"": {
+ ""provider"": ""StaticWebApps""
+ }
+ }
+ }" + "," +
+ EMPTY_ENTITIES_CONFIG_JSON +
+ "}";
+
+ ///
+ /// A minimal valid config json with nested mutations section as null.
+ ///
+ public const string BASE_CONFIG_NULL_NESTED_MUTATIONS_FIELD =
+ RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON +
+ @"
+ ""nested-mutations"": null
+ }," +
+ HOST_AND_ENTITY_OPTIONS_CONFIG_JSON;
+
+ ///
+ /// A minimal valid config json with an empty nested mutations section.
+ ///
+ public const string BASE_CONFIG_EMPTY_NESTED_MUTATIONS_FIELD =
+
+ RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON +
+ @"
+ ""nested-mutations"": {}
+ }," +
+ HOST_AND_ENTITY_OPTIONS_CONFIG_JSON;
+
+ ///
+ /// A minimal valid config json with the create field within nested mutation as null.
+ ///
+ public const string BASE_CONFIG_NULL_NESTED_CREATE_FIELD =
+
+ RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON +
+ @"
+ ""nested-mutations"": {
+ ""create"": null
+ }
+ }," +
+ HOST_AND_ENTITY_OPTIONS_CONFIG_JSON;
+
+ ///
+ /// A minimal valid config json with an empty create field within nested mutation.
+ ///
+ public const string BASE_CONFIG_EMPTY_NESTED_CREATE_FIELD =
+
+ RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON +
+ @"
+ ""nested-mutations"": {
+ ""create"": {}
+ }
+ }," +
+ HOST_AND_ENTITY_OPTIONS_CONFIG_JSON;
+
public static RuntimeConfigProvider GenerateInMemoryRuntimeConfigProvider(RuntimeConfig runtimeConfig)
{
MockFileSystem fileSystem = new();
diff --git a/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs
index e4366159eb..2f950b20d0 100644
--- a/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs
+++ b/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs
@@ -422,7 +422,12 @@ public static string GetModifiedJsonString(string[] reps, string enumString)
""graphql"": {
""enabled"": true,
""path"": """ + reps[++index % reps.Length] + @""",
- ""allow-introspection"": true
+ ""allow-introspection"": true,
+ ""nested-mutations"": {
+ ""create"": {
+ ""enabled"": false
+ }
+ }
},
""host"": {
""mode"": ""development"",
diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json
index dbec0174ef..0453d1ecf1 100644
--- a/src/Service.Tests/dab-config.MsSql.json
+++ b/src/Service.Tests/dab-config.MsSql.json
@@ -16,7 +16,12 @@
"graphql": {
"enabled": true,
"path": "/graphql",
- "allow-introspection": true
+ "allow-introspection": true,
+ "nested-mutations": {
+ "create": {
+ "enabled": true
+ }
+ }
},
"host": {
"cors": {