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": {