Skip to content

Commit 15d5fee

Browse files
CopilotJerryNixon
andcommitted
Fix: Allow parent config with data-source-files but no data-source
- Make DataSource and Entities parameters nullable in JsonConstructor - Defer validation when data-source-files is present - Adopt first child's DataSource as parent default when parent has none - Final validation ensures at least one DataSource exists after all loading - Add regression test for issue #2979 Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
1 parent 5169e58 commit 15d5fee

2 files changed

Lines changed: 85 additions & 15 deletions

File tree

src/Config/ObjectModel/RuntimeConfig.cs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -257,61 +257,68 @@ public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)]
257257
[JsonConstructor]
258258
public RuntimeConfig(
259259
string? Schema,
260-
DataSource DataSource,
261-
RuntimeEntities Entities,
260+
DataSource? DataSource,
261+
RuntimeEntities? Entities,
262262
RuntimeAutoentities? Autoentities = null,
263263
RuntimeOptions? Runtime = null,
264264
DataSourceFiles? DataSourceFiles = null,
265265
AzureKeyVaultOptions? AzureKeyVault = null)
266266
{
267267
this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK;
268-
this.DataSource = DataSource;
268+
this.DataSource = DataSource!;
269269
this.Runtime = Runtime;
270270
this.AzureKeyVault = AzureKeyVault;
271-
this.Entities = Entities;
271+
this.Entities = Entities!;
272272
this.Autoentities = Autoentities;
273273
this.DefaultDataSourceName = Guid.NewGuid().ToString();
274274

275-
if (this.DataSource is null)
275+
bool hasDataSourceFiles = DataSourceFiles?.SourceFiles?.Any() == true;
276+
277+
if (this.DataSource is null && !hasDataSourceFiles)
276278
{
277279
throw new DataApiBuilderException(
278280
message: "data-source is a mandatory property in DAB Config",
279281
statusCode: HttpStatusCode.UnprocessableEntity,
280282
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
281283
}
282284

283-
// we will set them up with default values
284-
_dataSourceNameToDataSource = new Dictionary<string, DataSource>
285-
{
286-
{ this.DefaultDataSourceName, this.DataSource }
287-
};
285+
// Initialize data source dictionary - may be empty if parent relies solely on data-source-files
286+
_dataSourceNameToDataSource = this.DataSource is not null
287+
? new Dictionary<string, DataSource> { { this.DefaultDataSourceName, this.DataSource } }
288+
: new Dictionary<string, DataSource>();
288289

289290
_entityNameToDataSourceName = new Dictionary<string, string>();
290-
if (Entities is null)
291+
292+
if (Entities is null && !hasDataSourceFiles)
291293
{
292294
throw new DataApiBuilderException(
293295
message: "entities is a mandatory property in DAB Config",
294296
statusCode: HttpStatusCode.UnprocessableEntity,
295297
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
296298
}
297299

298-
foreach (KeyValuePair<string, Entity> entity in Entities)
300+
if (Entities is null)
301+
{
302+
this.Entities = new RuntimeEntities(new Dictionary<string, Entity>());
303+
}
304+
305+
foreach (KeyValuePair<string, Entity> entity in this.Entities)
299306
{
300307
_entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName);
301308
}
302309

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

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

314-
foreach (string dataSourceFile in DataSourceFiles.SourceFiles)
321+
foreach (string dataSourceFile in DataSourceFiles!.SourceFiles!)
315322
{
316323
// Use default replacement settings for environment variable replacement
317324
DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true);
@@ -320,6 +327,13 @@ public RuntimeConfig(
320327
{
321328
try
322329
{
330+
// If parent has no DataSource, adopt first child's DataSource as the default.
331+
if (this.DataSource is null && config.DataSource is not null)
332+
{
333+
this.DataSource = config.DataSource;
334+
_dataSourceNameToDataSource[this.DefaultDataSourceName] = this.DataSource;
335+
}
336+
323337
_dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
324338
_entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
325339
allEntities = allEntities.Concat(config.Entities.AsEnumerable());
@@ -339,6 +353,15 @@ public RuntimeConfig(
339353
this.Entities = new RuntimeEntities(allEntities.ToDictionary(x => x.Key, x => x.Value));
340354
}
341355

356+
// Final validation: ensure at least one data source exists after loading all children
357+
if (this.DataSource is null)
358+
{
359+
throw new DataApiBuilderException(
360+
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.",
361+
statusCode: HttpStatusCode.UnprocessableEntity,
362+
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
363+
}
364+
342365
SetupDataSourcesUsed();
343366

344367
}

src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,51 @@ public async Task FailLoadMultiDataSourceConfigDuplicateEntities(string configPa
101101
Assert.IsTrue(error.StartsWith("Deserialization of the configuration file failed during a post-processing step."));
102102
Assert.IsTrue(error.Contains("An item with the same key has already been added."));
103103
}
104+
105+
/// <summary>
106+
/// Test validates that when parent config has no data-source but has data-source-files,
107+
/// the config loads correctly using the first child's data-source as the default.
108+
/// Regression test for: https://github.com/Azure/data-api-builder/issues/2979
109+
/// </summary>
110+
[DataTestMethod]
111+
[DataRow(new string[] { "Multidab-config.MsSql.json", "Multidab-config.MySql.json", "Multidab-config.PostgreSql.json" })]
112+
public void CanLoadMultiSourceConfigWithoutParentDataSource(IEnumerable<string> dataSourceFiles)
113+
{
114+
// Create a parent config with NO data-source, only data-source-files and runtime
115+
string parentConfig = @"{
116+
""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
117+
""data-source-files"": [" + string.Join(",", dataSourceFiles.Select(f => $"\"{f}\"")) + @"],
118+
""runtime"": {
119+
""rest"": {
120+
""enabled"": true,
121+
""path"": ""/api""
122+
},
123+
""graphql"": {
124+
""enabled"": true,
125+
""path"": ""/graphql""
126+
},
127+
""host"": {
128+
""mode"": ""development""
129+
}
130+
}
131+
}";
132+
133+
IFileSystem fs = new MockFileSystem(new Dictionary<string, MockFileData>()
134+
{
135+
{ "dab-config.json", new MockFileData(parentConfig) }
136+
});
137+
138+
FileSystemRuntimeConfigLoader loader = new(fs);
139+
140+
Assert.IsTrue(loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig), "Should successfully load config with data-source-files only");
141+
142+
// Verify: 1 default (adopted from first child) + 3 from children = 4
143+
Assert.AreEqual(4, runtimeConfig.ListAllDataSources().Count(), "Should have 4 data sources (1 default + 3 from children)");
144+
Assert.IsTrue(runtimeConfig.SqlDataSourceUsed, "Should have Sql data source");
145+
146+
// First child's data source becomes the parent default
147+
Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType, "Default datasource should be from first child file (MsSql)");
148+
149+
Assert.IsTrue(runtimeConfig.Entities.Any(), "Should have entities from child configs");
150+
}
104151
}

0 commit comments

Comments
 (0)