From 2f4430832b09d16542fe962c11330b646fa9793d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:29:30 +0000 Subject: [PATCH 1/2] Initial plan From 95bcddf498191643ac1ce811f630d67079ba0600 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:15:29 +0000 Subject: [PATCH 2/2] Enforce runtime config minimum property requirements Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> --- schemas/dab.draft.schema.json | 32 +++++++++---- src/Cli.Tests/TestHelper.cs | 16 +++++++ src/Cli.Tests/ValidateConfigTests.cs | 47 ++++++++++++++++++ src/Config/ObjectModel/RuntimeConfig.cs | 35 +++++++++----- src/Config/RuntimeConfigLoader.cs | 63 +++++++++++++------------ 5 files changed, 143 insertions(+), 50 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index fa5208af66..0173949786 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -1433,17 +1433,31 @@ } } }, - "if": { - "required": ["azure-key-vault"] - }, - "then": { - "properties": { - "azure-key-vault": { - "required": ["endpoint"] + "allOf": [ + { + "if": { "required": ["azure-key-vault"] }, + "then": { + "properties": { + "azure-key-vault": { + "required": ["endpoint"] + } + } } + }, + { + "if": { "not": { "required": ["data-source-files"] } }, + "then": { "required": ["data-source"] } + }, + { + "if": { + "allOf": [ + { "not": { "required": ["autoentities"] } }, + { "not": { "required": ["data-source-files"] } } + ] + }, + "then": { "required": ["entities"] } } - }, - "required": ["data-source", "entities"], + ], "$defs": { "singular-plural": { "oneOf": [ diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs index a75e359ee4..e2eb1fd544 100644 --- a/src/Cli.Tests/TestHelper.cs +++ b/src/Cli.Tests/TestHelper.cs @@ -279,6 +279,22 @@ public static Process ExecuteDabCommand(string command, string flags) public const string CONFIG_WITH_DISABLED_GLOBAL_REST_GRAPHQL = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{RUNTIME_SECTION_WITH_DISABLED_REST_GRAPHQL}}}"; + /// + /// A valid config json using autoentities instead of entities. data-source is still required. + /// + public const string CONFIG_WITH_AUTOENTITIES_ONLY = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{RUNTIME_SECTION}," + @"""autoentities"": { ""myAutoentity"": { ""patterns"": { ""include"": [""%.%""] } } } }"; + + /// + /// A valid multi-config (data-source-files) config without data-source or entities at the top level. + /// Only runtime is required in this scenario. + /// + public const string CONFIG_WITH_DATA_SOURCE_FILES_ONLY = $"{{{SCHEMA_PROPERTY},{RUNTIME_SECTION}," + @"""data-source-files"": [""child-config.json""] }"; + + /// + /// A config without data-source and without data-source-files. This is invalid. + /// + public const string CONFIG_WITHOUT_DATA_SOURCE_OR_DATA_SOURCE_FILES = $"{{{SCHEMA_PROPERTY},{RUNTIME_SECTION}," + @"""entities"": {} }"; + /// /// A config json with user-delegated-auth enabled. This is used in tests to verify updating existing /// user-delegated-auth configuration. diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs index e40a32e291..b14f5bba20 100644 --- a/src/Cli.Tests/ValidateConfigTests.cs +++ b/src/Cli.Tests/ValidateConfigTests.cs @@ -199,6 +199,53 @@ public void TestValidateConfigFailsWithNoEntities() } } + /// + /// Verifies that a config using autoentities (without an entities section) passes validation. + /// When autoentities is present, data-source is required but entities is not. + /// + [TestMethod] + public void TestValidateConfigPassesWithAutoentitiesAndNoEntities() + { + ((MockFileSystem)_fileSystem!).AddFile(TEST_RUNTIME_CONFIG_FILE, CONFIG_WITH_AUTOENTITIES_ONLY); + + try + { + // Config with autoentities and data-source but no entities should parse successfully. + // Full validation may fail due to no live database, but config parsing should succeed. + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( + _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE), out RuntimeConfig? config)); + Assert.IsNotNull(config); + Assert.IsTrue(config.Autoentities.Autoentities.Count > 0); + } + catch (Exception ex) + { + Assert.Fail($"Unexpected Exception thrown: {ex.Message}"); + } + } + + /// + /// Verifies that a multi-config (data-source-files) config without data-source or entities passes parsing. + /// When data-source-files is present, only runtime is required. + /// + [TestMethod] + public void TestValidateConfigPassesWithDataSourceFilesOnly() + { + ((MockFileSystem)_fileSystem!).AddFile(TEST_RUNTIME_CONFIG_FILE, CONFIG_WITH_DATA_SOURCE_FILES_ONLY); + + try + { + // Config with data-source-files but no data-source or entities should parse successfully. + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( + _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE), out RuntimeConfig? config)); + Assert.IsNotNull(config); + Assert.IsNotNull(config.DataSourceFiles); + } + catch (Exception ex) + { + Assert.Fail($"Unexpected Exception thrown: {ex.Message}"); + } + } + /// /// This Test is used to verify that the validate command is able to catch when data source field is missing. /// diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 46ff5a8dda..450113818e 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -267,29 +267,32 @@ public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, str /// To be used when setting up from cli json scenario. /// /// schema for config. - /// Default datasource. + /// Default datasource. May be null when data-source-files is specified (multi-config scenario). /// Entities /// Runtime settings. /// List of datasource files for multiple db scenario. Null for single db scenario. +#pragma warning disable CS8618 // DataSource may be null for multi-config (data-source-files) scenarios; callers should use ListAllDataSources() in that case. [JsonConstructor] public RuntimeConfig( string? Schema, - DataSource DataSource, - RuntimeEntities Entities, + DataSource? DataSource, + RuntimeEntities? Entities, RuntimeAutoentities? Autoentities = null, RuntimeOptions? Runtime = null, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null) +#pragma warning restore CS8618 { this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK; - this.DataSource = DataSource; + this.DataSource = DataSource!; // May be null for multi-config (data-source-files) scenarios. this.Runtime = Runtime; this.AzureKeyVault = AzureKeyVault; this.Entities = Entities ?? new RuntimeEntities(new Dictionary()); this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary()); this.DefaultDataSourceName = Guid.NewGuid().ToString(); - if (this.DataSource is null) + // data-source is required unless data-source-files is specified (multi-config scenario). + if (this.DataSource is null && DataSourceFiles is null) { throw new DataApiBuilderException( message: "data-source is a mandatory property in DAB Config", @@ -297,14 +300,18 @@ public RuntimeConfig( subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } - // we will set them up with default values - _dataSourceNameToDataSource = new Dictionary + // Initialize data source dictionary; only add the default data source when it is present. + _dataSourceNameToDataSource = new Dictionary(); + if (this.DataSource is not null) { - { this.DefaultDataSourceName, this.DataSource } - }; + _dataSourceNameToDataSource.Add(this.DefaultDataSourceName, this.DataSource); + } _entityNameToDataSourceName = new Dictionary(); - if (Entities is null && this.Entities.Entities.Count == 0 && + // entities/autoentities are required unless data-source-files is present (sub-configs supply them) + // or autoentities is present (entities are not required alongside autoentities). + if (DataSourceFiles is null && + Entities is null && this.Entities.Entities.Count == 0 && Autoentities is null && this.Autoentities.Autoentities.Count == 0) { throw new DataApiBuilderException( @@ -353,8 +360,12 @@ public RuntimeConfig( _dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); _entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); _autoentityNameToDataSourceName = _autoentityNameToDataSourceName.Concat(config._autoentityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - allEntities = allEntities?.Concat(config.Entities.AsEnumerable()); - allAutoentities = allAutoentities?.Concat(config.Autoentities.AsEnumerable()); + allEntities = allEntities == null + ? config.Entities.AsEnumerable() + : allEntities.Concat(config.Entities.AsEnumerable()); + allAutoentities = allAutoentities == null + ? config.Autoentities.AsEnumerable() + : allAutoentities.Concat(config.Autoentities.AsEnumerable()); } catch (Exception e) { diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index ae5c2dde95..6cadbdc924 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -219,43 +219,48 @@ public static bool TryParseConfig(string json, return false; } - // retreive current connection string from config - string updatedConnectionString = config.DataSource.ConnectionString; - - if (!string.IsNullOrEmpty(connectionString)) + // For single-datasource configs, update the default data source's connection string. + // Multi-config (data-source-files) scenarios may have no default DataSource. + if (config.DataSource is not null) { - // update connection string if provided. - updatedConnectionString = connectionString; - } + // retreive current connection string from config + string updatedConnectionString = config.DataSource.ConnectionString; - Dictionary datasourceNameToConnectionString = new(); + if (!string.IsNullOrEmpty(connectionString)) + { + // update connection string if provided. + updatedConnectionString = connectionString; + } - // add to dictionary if datasourceName is present - datasourceNameToConnectionString.TryAdd(config.DefaultDataSourceName, updatedConnectionString); + Dictionary datasourceNameToConnectionString = new(); - // iterate over dictionary and update runtime config with connection strings. - foreach ((string dataSourceKey, string connectionValue) in datasourceNameToConnectionString) - { - string updatedConnection = connectionValue; + // add to dictionary if datasourceName is present + datasourceNameToConnectionString.TryAdd(config.DefaultDataSourceName, updatedConnectionString); - DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey); - - // Add Application Name for telemetry for MsSQL or PgSql - if (ds.DatabaseType is DatabaseType.MSSQL && replacementSettings?.DoReplaceEnvVar == true) - { - updatedConnection = GetConnectionStringWithApplicationName(connectionValue); - } - else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true) + // iterate over dictionary and update runtime config with connection strings. + foreach ((string dataSourceKey, string connectionValue) in datasourceNameToConnectionString) { - updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue); - } + string updatedConnection = connectionValue; - ds = ds with { ConnectionString = updatedConnection }; - config.UpdateDataSourceNameToDataSource(config.DefaultDataSourceName, ds); + DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey); - if (string.Equals(dataSourceKey, config.DefaultDataSourceName, StringComparison.OrdinalIgnoreCase)) - { - config = config with { DataSource = ds }; + // Add Application Name for telemetry for MsSQL or PgSql + if (ds.DatabaseType is DatabaseType.MSSQL && replacementSettings?.DoReplaceEnvVar == true) + { + updatedConnection = GetConnectionStringWithApplicationName(connectionValue); + } + else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true) + { + updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue); + } + + ds = ds with { ConnectionString = updatedConnection }; + config.UpdateDataSourceNameToDataSource(config.DefaultDataSourceName, ds); + + if (string.Equals(dataSourceKey, config.DefaultDataSourceName, StringComparison.OrdinalIgnoreCase)) + { + config = config with { DataSource = ds }; + } } } }