Skip to content
Merged
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
17 changes: 8 additions & 9 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,12 +368,10 @@ public RuntimeConfig(

if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null)
{
IEnumerable<KeyValuePair<string, Entity>>? allEntities = Entities?.AsEnumerable();
IEnumerable<KeyValuePair<string, Autoentity>>? allAutoentities = Autoentities?.AsEnumerable();
IEnumerable<KeyValuePair<string, Entity>>? allEntities = Entities is null ? new List<KeyValuePair<string, Entity>>().AsEnumerable() : Entities.AsEnumerable();
IEnumerable<KeyValuePair<string, Autoentity>>? allAutoentities = Autoentities is null ? new List<KeyValuePair<string, Autoentity>>().AsEnumerable() : Autoentities.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);

// Pass the parent's AKV options so @akv() references in child configs can
// be resolved using the parent's Key Vault configuration.
Expand All @@ -390,7 +388,8 @@ public RuntimeConfig(

foreach (string dataSourceFile in DataSourceFiles.SourceFiles)
{

// This loader is not used as a part of hot reload and therefore does not need a handler.
FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null, baseConfigFilePath: dataSourceFile);
if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings))
{
try
Expand All @@ -404,8 +403,8 @@ 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.Concat(config.Entities.AsEnumerable());
allAutoentities = allAutoentities.Concat(config.Autoentities.AsEnumerable());
}
catch (Exception e)
{
Expand All @@ -430,8 +429,8 @@ public RuntimeConfig(
}
}

this.Entities = new RuntimeEntities(allEntities != null ? allEntities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Entity>());
this.Autoentities = new RuntimeAutoentities(allAutoentities != null ? allAutoentities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Autoentity>());
this.Entities = new RuntimeEntities(allEntities.ToDictionary(x => x.Key, x => x.Value));
this.Autoentities = new RuntimeAutoentities(allAutoentities.ToDictionary(x => x.Key, x => x.Value));
}

SetupDataSourcesUsed();
Expand Down
14 changes: 12 additions & 2 deletions src/Core/Services/MetadataProviders/MetadataProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,21 @@ private void ConfigureMetadataProviders()
{
foreach ((string dataSourceName, DataSource dataSource) in _runtimeConfigProvider.GetConfig().GetDataSourceNamesToDataSourcesIterator())
{
RuntimeConfig? childConfig = null;
foreach ((string fieldName, RuntimeConfig currentChildConfig) in _runtimeConfigProvider.GetConfig().ChildConfigs)
{
if (dataSourceName == currentChildConfig.DefaultDataSourceName)
{
childConfig = currentChildConfig;
break;
}
}

ISqlMetadataProvider metadataProvider = dataSource.DatabaseType switch
{
DatabaseType.CosmosDB_NoSQL => new CosmosSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _fileSystem),
DatabaseType.MSSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
DatabaseType.DWSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
DatabaseType.MSSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly, childConfig),
DatabaseType.DWSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly, childConfig),
DatabaseType.PostgreSQL => new PostgreSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
DatabaseType.MySQL => new MySqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
_ => throw new NotSupportedException(dataSource.DatabaseTypeNotSupportedMessage),
Expand Down
10 changes: 9 additions & 1 deletion src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@ public class MsSqlMetadataProvider :
{
private RuntimeConfigProvider _runtimeConfigProvider;

private RuntimeConfig? _childConfig;

public MsSqlMetadataProvider(
RuntimeConfigProvider runtimeConfigProvider,
RuntimeConfigValidator runtimeConfigValidator,
IAbstractQueryManagerFactory queryManagerFactory,
ILogger<ISqlMetadataProvider> logger,
string dataSourceName,
bool isValidateOnly = false)
bool isValidateOnly = false,
RuntimeConfig? childConfig = null)
: base(runtimeConfigProvider, runtimeConfigValidator, queryManagerFactory, logger, dataSourceName, isValidateOnly)
{
_runtimeConfigProvider = runtimeConfigProvider;
_childConfig = childConfig;
}

public override string GetDefaultSchemaName()
Expand Down Expand Up @@ -464,6 +468,10 @@ protected override async Task GenerateAutoentitiesIntoEntities(

// Track resolution count for validation.
runtimeConfig.AutoentityResolutionCounts[autoentityName] = addedEntities;
if (_childConfig is not null)
{
_childConfig.AutoentityResolutionCounts[autoentityName] = addedEntities;
}
}

LogRestPathsForEntities(runtimeConfig, entities);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,8 @@ private static Mock<SqlQueryStructure> CreateMockSqlQueryStructure(string entity
mockQueryFactory.Object,
mockLogger.Object,
dataSourceName,
isValidateOnly);
isValidateOnly,
null);
mockSqlMetadataProvider
.Setup(s => s.EntityToDatabaseObject)
.Returns(entityToDatabaseObject);
Expand Down
126 changes: 126 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6651,6 +6651,132 @@ public async Task ValidateAutoentitiesConfiguration()
"Unexpected autoentity-related validation error.");
}

/// <summary>
/// End-to-end regression test that creates actual root and child JSON files, loads them through
/// <see cref="FileSystemRuntimeConfigLoader"/>, runs metadata initialization and validation via
/// <see cref="RuntimeConfigValidator.TryValidateConfig"/>, and asserts the PER-FILE result.
///
/// The root references the child through <c>data-source-files</c>. The root's own autoentity
/// resolves ZERO entities (its pattern matches no table) while the child's autoentities for MSSQL
/// resolve real tables (dbo.books). After the child's autoentities are merged into the root, the
/// merged config has resolvable entities, so validation succeeds and no "No entities found"
/// presence error is produced.
/// Requires a running MSSQL instance reachable via the standard MSSQL test connection string.
/// </summary>
[TestMethod]
[TestCategory(TestCategory.MSSQL)]
public async Task TestValidate_MultiConfigRootResolvingZero_ProducesRootScopedErrorAndValidChild()
{
(string rootConfigPath, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem, string tempDir) =
ArrangeMultiConfigForMsSql();

ILoggerFactory loggerFactory = new LoggerFactory();
try
{
loader.UpdateConfigFilePath(rootConfigPath);
RuntimeConfigProvider provider = new(loader);
Assert.AreEqual(1, provider.GetConfig().ChildConfigs.Count, "Expected exactly one child config to be resolved and merged.");

RuntimeConfigValidator validator = new(
provider,
fileSystem,
loggerFactory.CreateLogger<RuntimeConfigValidator>(),
isValidateOnly: true);

bool isValid = await validator.TryValidateConfig(rootConfigPath, loggerFactory);
Assert.IsTrue(isValid, "Validation should succeed");

List<Exception> presenceErrors = validator.ConfigValidationExceptions
.Where(e => e.Message.Contains("No entities found"))
.ToList();
Assert.AreEqual(0, presenceErrors.Count, "Expected no errors to be found");
}
finally
{
loggerFactory.Dispose();

// Guarantee cleanup so fixed-name/leftover files can never leak into another test or run.
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}

/// <summary>
/// Helper: builds a real multi-config on disk for MSSQL directly through the
/// <see cref="RuntimeConfig"/> object model (the pattern used throughout these Service.Tests),
/// rather than the CLI generators which are not referenced by this test project:
/// - a CHILD config with its own MSSQL data source and autoentities matching <c>dbo.books</c>,
/// - a ROOT config with its own MSSQL data source, autoentities matching
/// <paramref name="rootPatternInclude"/>, and <c>data-source-files</c> pointing at the child.
Comment thread
RubenCerna2079 marked this conversation as resolved.
/// Returns the root config path plus a fresh loader and file system to drive validation.
/// </summary>
private static (string RootConfigPath, FileSystemRuntimeConfigLoader ValidateLoader, IFileSystem FileSystem, string tempDir)
ArrangeMultiConfigForMsSql()
{
string connectionString = GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL);
IFileSystem fileSystem = new FileSystem();

// Unique directory per invocation avoids cross-test/cross-run interference from leftover files.
string tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "dab-multiconfig-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);

string rootConfigPath = System.IO.Path.Combine(tempDir, "dab-root.json");
string childConfigPath = System.IO.Path.Combine(tempDir, "dab-child.json");

// Root: own MSSQL data source + autoentities (pattern controls whether it resolves) +
// data-source-files pointing at the child.
RuntimeConfig rootConfig = new(
Schema: "root-schema",
DataSource: new(DatabaseType.MSSQL, connectionString, Options: null),
Entities: new(new Dictionary<string, Entity>()),
Autoentities: new(BuildAutoentityMap(definitionName: "root-filter", patternInclude: "dbo.books", entiityNames: "root_{object}")),
Comment thread
RubenCerna2079 marked this conversation as resolved.
Runtime: new(
Rest: new(),
GraphQL: new(),
Mcp: new(),
Host: new(null, null, HostMode.Development)),
DataSourceFiles: new DataSourceFiles(new[] { childConfigPath }));
File.WriteAllText(rootConfigPath, rootConfig.ToJson());

// Child: own MSSQL data source + autoentities matching a real table (dbo.books).
RuntimeConfig childConfig = new(
Schema: "child-schema",
DataSource: new(DatabaseType.MSSQL, connectionString, Options: null),
Entities: new(new Dictionary<string, Entity>()),
Autoentities: new(BuildAutoentityMap(definitionName: "child-filter", patternInclude: "dbo.books", entiityNames: "child_{object}")),
Runtime: new(
Rest: new(),
GraphQL: new(),
Mcp: new(),
Host: new(null, null, HostMode.Development)));
File.WriteAllText(childConfigPath, childConfig.ToJson());

return (rootConfigPath, new FileSystemRuntimeConfigLoader(fileSystem), fileSystem, tempDir);
}

/// <summary>
/// Helper: builds an autoentity map containing a single definition whose include pattern
/// controls which tables it resolves against the target database.
/// </summary>
private static Dictionary<string, Autoentity> BuildAutoentityMap(string definitionName, string patternInclude, string entiityNames)
Comment thread
RubenCerna2079 marked this conversation as resolved.
{
EntityAction entityAction = new(EntityActionOperation.Read, null, null);

Autoentity autoentity = new(
Patterns: new AutoentityPatterns(
Include: new[] { patternInclude },
Exclude: Array.Empty<string>(),
Name: entiityNames),
Template: new AutoentityTemplate(
Rest: new(Enabled: true),
GraphQL: new(Enabled: true, Singular: string.Empty, Plural: string.Empty)),
Permissions: new EntityPermission[] { new("anonymous", new EntityAction[] { entityAction }) });

return new Dictionary<string, Autoentity> { { definitionName, autoentity } };
}

/// <summary>
/// Tests the behavior of GraphQL queries in non-hosted mode when the depth limit is explicitly set to -1 or null.
/// Setting the depth limit to -1 is intended to disable the depth limit check, allowing queries of any depth.
Expand Down