Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
13 changes: 10 additions & 3 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ Runtime.GraphQL.FeatureFlags is not null &&

private Dictionary<string, string> _entityPathNameToEntityName = new();

private Dictionary<string, FileSystemRuntimeConfigLoader> _dataSourceNameToConfigLoader = new();

/// <summary>
/// List of all datasources.
/// </summary>
Expand Down Expand Up @@ -305,6 +307,11 @@ public bool RemoveGeneratedAutoentityNameFromDataSourceName(string entityName)
return _entityNameToDataSourceName.Remove(entityName);
}

public bool TryGetConfigLoaderFromDataSourceName(string dataSourceName, [NotNullWhen(true)] out FileSystemRuntimeConfigLoader? configLoader)
{
return _dataSourceNameToConfigLoader.TryGetValue(dataSourceName, out configLoader);
}

/// <summary>
/// Constructor for runtimeConfig.
/// To be used when setting up from cli json scenario.
Expand Down Expand Up @@ -372,8 +379,6 @@ public RuntimeConfig(
IEnumerable<KeyValuePair<string, Autoentity>>? allAutoentities = 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 +395,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 @@ -401,6 +407,7 @@ public RuntimeConfig(
// Store the child config reference for per-child validation.
ChildConfigs.Add((dataSourceFile, config));

_dataSourceNameToConfigLoader.TryAdd(config.DefaultDataSourceName, loader);
_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);
Expand Down
16 changes: 11 additions & 5 deletions src/Core/Services/MetadataProviders/MetadataProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,19 @@ private void ConfigureMetadataProviders()
{
foreach ((string dataSourceName, DataSource dataSource) in _runtimeConfigProvider.GetConfig().GetDataSourceNamesToDataSourcesIterator())
{
RuntimeConfigProvider runtimeConfigProvider = _runtimeConfigProvider;
if (_runtimeConfigProvider.GetConfig().TryGetConfigLoaderFromDataSourceName(dataSourceName, out FileSystemRuntimeConfigLoader? configLoader))
{
runtimeConfigProvider = new RuntimeConfigProvider(configLoader);
}

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.PostgreSQL => new PostgreSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
DatabaseType.MySQL => new MySqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
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.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
112 changes: 112 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6655,6 +6655,118 @@ 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()
{
// Root autoentity matches nothing (resolves 0); child matches dbo.books (resolves).
(string rootConfigPath, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) =
ArrangeMultiConfigForMsSql();

// Point the loader at the root config and load through the provider so the child is merged.
loader.UpdateConfigFilePath(rootConfigPath);
RuntimeConfigProvider provider = new(loader);

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

// Runs metadata initialization (real autoentity resolution against MSSQL) + presence validation.
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");
}

/// <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.
/// Returns the root config path plus a fresh loader and file system to drive validation.
/// </summary>
private static (string RootConfigPath, FileSystemRuntimeConfigLoader ValidateLoader, IFileSystem FileSystem)
ArrangeMultiConfigForMsSql()
{
string connectionString = GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL);

// Use a real file system + temp directory so the RuntimeConfig constructor (which loads
// data-source-files through the real file system) can find and merge the child config.
IFileSystem fileSystem = new FileSystem();

// 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}")),
Runtime: new(
Rest: new(),
GraphQL: new(),
Mcp: new(),
Host: new(null, null, HostMode.Development)),
DataSourceFiles: new DataSourceFiles(new[] { "dab-child.json" }));
File.WriteAllText("dab-root.json", 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("dab-child.json", childConfig.ToJson());

return ("dab-root.json", new FileSystemRuntimeConfigLoader(fileSystem), fileSystem);
}

/// <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)
{
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