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
32 changes: 23 additions & 9 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
16 changes: 16 additions & 0 deletions src/Cli.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}";

/// <summary>
/// A valid config json using autoentities instead of entities. data-source is still required.
/// </summary>
public const string CONFIG_WITH_AUTOENTITIES_ONLY = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{RUNTIME_SECTION}," + @"""autoentities"": { ""myAutoentity"": { ""patterns"": { ""include"": [""%.%""] } } } }";

/// <summary>
/// A valid multi-config (data-source-files) config without data-source or entities at the top level.
/// Only runtime is required in this scenario.
/// </summary>
public const string CONFIG_WITH_DATA_SOURCE_FILES_ONLY = $"{{{SCHEMA_PROPERTY},{RUNTIME_SECTION}," + @"""data-source-files"": [""child-config.json""] }";

/// <summary>
/// A config without data-source and without data-source-files. This is invalid.
/// </summary>
public const string CONFIG_WITHOUT_DATA_SOURCE_OR_DATA_SOURCE_FILES = $"{{{SCHEMA_PROPERTY},{RUNTIME_SECTION}," + @"""entities"": {} }";

/// <summary>
/// A config json with user-delegated-auth enabled. This is used in tests to verify updating existing
/// user-delegated-auth configuration.
Expand Down
47 changes: 47 additions & 0 deletions src/Cli.Tests/ValidateConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,53 @@ public void TestValidateConfigFailsWithNoEntities()
}
}

/// <summary>
/// Verifies that a config using autoentities (without an entities section) passes validation.
/// When autoentities is present, data-source is required but entities is not.
/// </summary>
[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}");
}
}

/// <summary>
/// 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.
/// </summary>
[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}");
}
}

/// <summary>
/// This Test is used to verify that the validate command is able to catch when data source field is missing.
/// </summary>
Expand Down
35 changes: 23 additions & 12 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,44 +267,51 @@ public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, str
/// To be used when setting up from cli json scenario.
/// </summary>
/// <param name="Schema">schema for config.</param>
/// <param name="DataSource">Default datasource.</param>
/// <param name="DataSource">Default datasource. May be null when data-source-files is specified (multi-config scenario).</param>
/// <param name="Entities">Entities</param>
/// <param name="Runtime">Runtime settings.</param>
/// <param name="DataSourceFiles">List of datasource files for multiple db scenario. Null for single db scenario.</param>
#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<string, Entity>());
this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary<string, Autoentity>());
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",
statusCode: HttpStatusCode.UnprocessableEntity,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}

// we will set them up with default values
_dataSourceNameToDataSource = new Dictionary<string, DataSource>
// Initialize data source dictionary; only add the default data source when it is present.
_dataSourceNameToDataSource = new Dictionary<string, DataSource>();
if (this.DataSource is not null)
{
{ this.DefaultDataSourceName, this.DataSource }
};
_dataSourceNameToDataSource.Add(this.DefaultDataSourceName, this.DataSource);
}

_entityNameToDataSourceName = new Dictionary<string, string>();
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(
Expand Down Expand Up @@ -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)
{
Expand Down
63 changes: 34 additions & 29 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> 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<string, string> 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 };
}
}
}
}
Expand Down