Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,46 +272,64 @@ public RuntimeConfig(
this.Autoentities = Autoentities;
this.DefaultDataSourceName = Guid.NewGuid().ToString();

if (this.DataSource is null)
bool hasDataSourceFiles = DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null && DataSourceFiles.SourceFiles.Any();

// Only require data-source if data-source-files is not provided
if (this.DataSource is null && !hasDataSourceFiles)
{
throw new DataApiBuilderException(
message: "data-source is a mandatory property in DAB Config",
statusCode: HttpStatusCode.UnprocessableEntity,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}

// we will set them up with default values
_dataSourceNameToDataSource = new Dictionary<string, DataSource>
// Initialize data source dictionary - may be empty if parent relies solely on data-source-files
if (this.DataSource is not null)
{
_dataSourceNameToDataSource = new Dictionary<string, DataSource>
{
{ this.DefaultDataSourceName, this.DataSource }
};
}
else
{
{ this.DefaultDataSourceName, this.DataSource }
};
_dataSourceNameToDataSource = new Dictionary<string, DataSource>();
}

_entityNameToDataSourceName = new Dictionary<string, string>();
if (Entities is null)

// Only require entities if data-source-files is not provided
if (Entities is null && !hasDataSourceFiles)
{
throw new DataApiBuilderException(
message: "entities is a mandatory property in DAB Config",
statusCode: HttpStatusCode.UnprocessableEntity,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}

foreach (KeyValuePair<string, Entity> entity in Entities)
// Initialize entities to empty if null (when using data-source-files)
if (Entities is null)
{
this.Entities = new RuntimeEntities(new Dictionary<string, Entity>());
}

foreach (KeyValuePair<string, Entity> entity in this.Entities)
{
_entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName);
}

// Process data source and entities information for each database in multiple database scenario.
this.DataSourceFiles = DataSourceFiles;

if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null)
if (hasDataSourceFiles)
{
IEnumerable<KeyValuePair<string, Entity>> allEntities = Entities.AsEnumerable();
IEnumerable<KeyValuePair<string, Entity>> allEntities = this.Entities.AsEnumerable();
// Iterate through all the datasource files and load the config.
IFileSystem fileSystem = new FileSystem();
// This loader is not used as a part of hot reload and therefore does not need a handler.
FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null);

foreach (string dataSourceFile in DataSourceFiles.SourceFiles)
foreach (string dataSourceFile in DataSourceFiles!.SourceFiles!)
{
// Use default replacement settings for environment variable replacement
DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true);
Expand All @@ -320,6 +338,14 @@ public RuntimeConfig(
{
try
{
// If parent has no DataSource, use the first child's DataSource as the default.
// This only happens once - subsequent children skip this block since this.DataSource is no longer null.
if (this.DataSource is null && config.DataSource is not null)
{
this.DataSource = config.DataSource;
_dataSourceNameToDataSource[this.DefaultDataSourceName] = this.DataSource;
}

_dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
allEntities = allEntities.Concat(config.Entities.AsEnumerable());
Expand All @@ -339,6 +365,15 @@ public RuntimeConfig(
this.Entities = new RuntimeEntities(allEntities.ToDictionary(x => x.Key, x => x.Value));
}

// Final validation: ensure we have at least one data source after all loading
if (this.DataSource is null)
{
throw new DataApiBuilderException(
message: "data-source is a mandatory property in DAB Config. When using data-source-files, at least one child config must contain a valid data-source.",
statusCode: HttpStatusCode.UnprocessableEntity,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}

SetupDataSourcesUsed();

}
Expand Down
65 changes: 65 additions & 0 deletions src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,69 @@ public async Task FailLoadMultiDataSourceConfigDuplicateEntities(string configPa
Assert.IsTrue(error.StartsWith("Deserialization of the configuration file failed during a post-processing step."));
Assert.IsTrue(error.Contains("An item with the same key has already been added."));
}

/// <summary>
/// Test validates that when parent config has no data-source but has data-source-files,
/// the config loads correctly using the first child's data-source as the default.
/// This is the scenario from GitHub issue: Multiple Source Files Config not loading.
/// </summary>
[DataTestMethod]
[DataRow(new string[] { "Multidab-config.MsSql.json", "Multidab-config.MySql.json", "Multidab-config.PostgreSql.json" })]
public async Task CanLoadMultiSourceConfigWithoutParentDataSource(IEnumerable<string> dataSourceFiles)
{
// Create a parent config with NO data-source, only data-source-files and runtime
string parentConfig = @"{
""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
""data-source-files"": [" + string.Join(",", dataSourceFiles.Select(f => $"\"{f}\"")) + @"],
""runtime"": {
""rest"": {
""enabled"": true,
""path"": ""/api""
},
""graphql"": {
""enabled"": true,
""path"": ""/graphql""
},
""host"": {
""mode"": ""development""
}
}
}";

// Load all child config files
Dictionary<string, MockFileData> mockFiles = new()
{
{ "dab-config.json", new MockFileData(parentConfig) }
};

foreach (string dataSourceFile in dataSourceFiles)
{
string childContent = await File.ReadAllTextAsync(dataSourceFile);
mockFiles.Add(dataSourceFile, new MockFileData(childContent));
}

IFileSystem fs = new MockFileSystem(mockFiles);
FileSystemRuntimeConfigLoader loader = new(fs);

Assert.IsTrue(loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig), "Should successfully load config with data-source-files only");

// Verify the config loaded correctly
// When parent has no data-source, we get:
// - One entry for parent's DefaultDataSourceName (set from first child's data source)
// - Three entries from child configs (each with their own DefaultDataSourceName)
IList<DataSource> allDataSources = runtimeConfig.ListAllDataSources().ToList();
Assert.AreEqual(4, allDataSources.Count, "Should have 4 data sources (1 default + 3 from children)");
Assert.IsTrue(runtimeConfig.SqlDataSourceUsed, "Should have Sql data source");

// Verify all three SQL database types from children are present
Assert.IsTrue(allDataSources.Any(ds => ds.DatabaseType == DatabaseType.MSSQL), "Should have MsSql data source");
Assert.IsTrue(allDataSources.Any(ds => ds.DatabaseType == DatabaseType.MySQL), "Should have MySql data source");
Assert.IsTrue(allDataSources.Any(ds => ds.DatabaseType == DatabaseType.PostgreSQL), "Should have PostgreSql data source");

// Verify the first child's data source is used as the default
Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType, "Default datasource should be from first child file (MsSql)");

// Verify entities were loaded from children
Assert.IsTrue(runtimeConfig.Entities.Any(), "Should have entities from child configs");
}
}