diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index f5112844da..20af5c9911 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -96,18 +96,6 @@ public void ValidateConfigProperties() ValidateLoggerFilters(runtimeConfig); ValidateAzureLogAnalyticsAuth(runtimeConfig); ValidateFileSinkPath(runtimeConfig); - - // Running these graphQL validations only in development mode to ensure - // fast startup of engine in production mode. - if (runtimeConfig.IsDevelopmentMode()) - { - ValidateEntityConfiguration(runtimeConfig); - - if (runtimeConfig.IsGraphQLEnabled) - { - ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(runtimeConfig.DataSource.DatabaseType, runtimeConfig.Entities); - } - } } /// @@ -499,6 +487,7 @@ public async Task ValidateEntitiesMetadata(RuntimeConfig runtimeConfig, ILoggerF // Only used for validation so we don't need the handler which is for hot reload scenarios. MetadataProviderFactory metadataProviderFactory = new( runtimeConfigProvider: _runtimeConfigProvider, + runtimeConfigValidator: this, queryManagerFactory: queryManagerFactory, logger: loggerFactory.CreateLogger(), fileSystem: _fileSystem, @@ -1594,4 +1583,26 @@ private static bool IsLoggerFilterValid(string loggerFilter) return false; } + + /// + /// Checks that all of the entities created with the Entities and Autoentities properties + /// are valid by having unique paths for both REST and GraphQL, that there are no duplicate + /// Queries or Mutation entities, and ensure the semantic correctness of all the entities. + /// + /// The runtime configuration. + public void ValidateEntityAndAutoentityConfigurations(RuntimeConfig runtimeConfig) + { + if (runtimeConfig.IsDevelopmentMode()) + { + ValidateEntityConfiguration(runtimeConfig); + + if (runtimeConfig.IsGraphQLEnabled) + { + ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(runtimeConfig.DataSource.DatabaseType, runtimeConfig.Entities); + } + + // Running only in developer mode to ensure fast and smooth startup in production. + ValidatePermissionsInConfig(runtimeConfig); + } + } } diff --git a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 61ffeeab09..02d3226af0 100644 --- a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -55,7 +55,7 @@ public class CosmosSqlMetadataProvider : ISqlMetadataProvider public List SqlMetadataExceptions { get; private set; } = new(); - public CosmosSqlMetadataProvider(RuntimeConfigProvider runtimeConfigProvider, IFileSystem fileSystem) + public CosmosSqlMetadataProvider(RuntimeConfigProvider runtimeConfigProvider, RuntimeConfigValidator runtimeConfigValidator, IFileSystem fileSystem) { RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig(); _fileSystem = fileSystem; @@ -76,6 +76,8 @@ public CosmosSqlMetadataProvider(RuntimeConfigProvider runtimeConfigProvider, IF subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } + runtimeConfigValidator.ValidateEntityAndAutoentityConfigurations(runtimeConfig); + _cosmosDb = cosmosDb; ParseSchemaGraphQLDocument(); diff --git a/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs b/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs index 66112fce21..6fe20969ed 100644 --- a/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs +++ b/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs @@ -19,6 +19,7 @@ public class MetadataProviderFactory : IMetadataProviderFactory { private readonly IDictionary _metadataProviders; private readonly RuntimeConfigProvider _runtimeConfigProvider; + private readonly RuntimeConfigValidator _runtimeConfigValidator; private readonly IAbstractQueryManagerFactory _queryManagerFactory; private readonly ILogger _logger; private readonly IFileSystem _fileSystem; @@ -26,6 +27,7 @@ public class MetadataProviderFactory : IMetadataProviderFactory public MetadataProviderFactory( RuntimeConfigProvider runtimeConfigProvider, + RuntimeConfigValidator runtimeConfigValidator, IAbstractQueryManagerFactory queryManagerFactory, ILogger logger, IFileSystem fileSystem, @@ -34,6 +36,7 @@ public MetadataProviderFactory( { handler?.Subscribe(METADATA_PROVIDER_FACTORY_ON_CONFIG_CHANGED, OnConfigChanged); _runtimeConfigProvider = runtimeConfigProvider; + _runtimeConfigValidator = runtimeConfigValidator; _queryManagerFactory = queryManagerFactory; _logger = logger; _fileSystem = fileSystem; @@ -48,11 +51,11 @@ private void ConfigureMetadataProviders() { ISqlMetadataProvider metadataProvider = dataSource.DatabaseType switch { - DatabaseType.CosmosDB_NoSQL => new CosmosSqlMetadataProvider(_runtimeConfigProvider, _fileSystem), - DatabaseType.MSSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly), - DatabaseType.DWSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly), - DatabaseType.PostgreSQL => new PostgreSqlMetadataProvider(_runtimeConfigProvider, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly), - DatabaseType.MySQL => new MySqlMetadataProvider(_runtimeConfigProvider, _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), }; diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index dad3eeb873..96fa47dcfd 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -33,11 +33,12 @@ public class MsSqlMetadataProvider : public MsSqlMetadataProvider( RuntimeConfigProvider runtimeConfigProvider, + RuntimeConfigValidator runtimeConfigValidator, IAbstractQueryManagerFactory queryManagerFactory, ILogger logger, string dataSourceName, bool isValidateOnly = false) - : base(runtimeConfigProvider, queryManagerFactory, logger, dataSourceName, isValidateOnly) + : base(runtimeConfigProvider, runtimeConfigValidator, queryManagerFactory, logger, dataSourceName, isValidateOnly) { _runtimeConfigProvider = runtimeConfigProvider; } @@ -374,7 +375,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (addedEntities == 0) { - _logger.LogWarning($"No new entities were generated from the autoentity {autoentityName} defined in the configuration."); + _logger.LogWarning("No new entities were generated from the autoentity {autoentityName} defined in the configuration.", autoentityName); } } diff --git a/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs index 99336180d5..26098c2d15 100644 --- a/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs @@ -23,11 +23,12 @@ public class MySqlMetadataProvider : SqlMetadataProvider logger, string dataSourceName, bool isValidateOnly = false) - : base(runtimeConfigProvider, queryManagerFactory, logger, dataSourceName, isValidateOnly) + : base(runtimeConfigProvider, runtimeConfigValidator, queryManagerFactory, logger, dataSourceName, isValidateOnly) { try { diff --git a/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs index ecd65b3d95..0d43d0efbc 100644 --- a/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs @@ -22,11 +22,12 @@ public class PostgreSqlMetadataProvider : public PostgreSqlMetadataProvider( RuntimeConfigProvider runtimeConfigProvider, + RuntimeConfigValidator runtimeConfigValidator, IAbstractQueryManagerFactory queryManagerFactory, ILogger logger, string dataSourceName, bool isValidateOnly = false) - : base(runtimeConfigProvider, queryManagerFactory, logger, dataSourceName, isValidateOnly) + : base(runtimeConfigProvider, runtimeConfigValidator, queryManagerFactory, logger, dataSourceName, isValidateOnly) { } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index c7bc023142..a804d5417a 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -77,6 +77,8 @@ public abstract class SqlMetadataProvider : private RuntimeConfigProvider _runtimeConfigProvider; + private RuntimeConfigValidator _runtimeConfigValidator; + private Dictionary> EntityBackingColumnsToExposedNames { get; } = new(); private Dictionary> EntityExposedNamesToBackingColumnNames { get; } = new(); @@ -108,6 +110,7 @@ private void HandleOrRecordException(Exception e) public SqlMetadataProvider( RuntimeConfigProvider runtimeConfigProvider, + RuntimeConfigValidator runtimeConfigValidator, IAbstractQueryManagerFactory engineFactory, ILogger logger, string dataSourceName, @@ -115,6 +118,7 @@ public SqlMetadataProvider( { RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig(); _runtimeConfigProvider = runtimeConfigProvider; + _runtimeConfigValidator = runtimeConfigValidator; _dataSourceName = dataSourceName; _databaseType = runtimeConfig.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType; _logger = logger; @@ -310,12 +314,7 @@ public string GetEntityName(string graphQLType) public async Task InitializeAsync() { System.Diagnostics.Stopwatch timer = System.Diagnostics.Stopwatch.StartNew(); - if (GetDatabaseType() == DatabaseType.MSSQL) - { - await GenerateAutoentitiesIntoEntities(Autoentities); - } - GenerateDatabaseObjectForEntities(); if (_isValidateOnly) { // Currently Validate mode only support single datasource, @@ -332,6 +331,18 @@ public async Task InitializeAsync() } } + if (GetDatabaseType() == DatabaseType.MSSQL) + { + await GenerateAutoentitiesIntoEntities(Autoentities); + } + + // Running these graphQL validations only in development mode to ensure + // fast startup of engine in production mode. + RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); + _runtimeConfigValidator.ValidateEntityAndAutoentityConfigurations(runtimeConfig); + + GenerateDatabaseObjectForEntities(); + await PopulateObjectDefinitionForEntities(); GenerateExposedToBackingColumnMapsForEntities(); // When IsLateConfigured is true we are in a hosted scenario and do not reveal primary key information. diff --git a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs index 68c9225b96..91c6ef28bd 100644 --- a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs +++ b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Data.Common; +using System.IO.Abstractions; using System.Net; using System.Reflection; using System.Text.Json; @@ -699,10 +700,14 @@ private static Mock CreateMockSqlQueryStructure(string entity entityToDatabaseObject.Add(entityName, new DatabaseTable()); Mock mockRuntimeConfigProvider = CreateMockRuntimeConfigProvider(entityName); + IFileSystem fileSystem = new FileSystem(); + Mock> loggerValidator = new(); + RuntimeConfigValidator runtimeConfigValidator = new(mockRuntimeConfigProvider.Object, fileSystem, loggerValidator.Object); Mock mockQueryFactory = new(); Mock> mockLogger = new(); Mock mockSqlMetadataProvider = new( mockRuntimeConfigProvider.Object, + runtimeConfigValidator, mockQueryFactory.Object, mockLogger.Object, dataSourceName, diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 6781c81675..c21a35a3d4 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -3681,8 +3681,11 @@ public void ValidateGraphQLSchemaForCircularReference(string schema) FileSystemRuntimeConfigLoader loader = new(fileSystem); RuntimeConfigProvider provider = new(loader); + Mock> loggerValidator = new(); + RuntimeConfigValidator validator = new(provider, fileSystem, loggerValidator.Object); + DataApiBuilderException exception = - Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, fileSystem)); + Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, validator, fileSystem)); Assert.AreEqual("Circular reference detected in the provided GraphQL schema for entity 'Character'.", exception.Message); Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode); Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, exception.SubStatusCode); @@ -3732,9 +3735,11 @@ type Planet @model(name:""PlanetAlias"") { }); FileSystemRuntimeConfigLoader loader = new(fileSystem); RuntimeConfigProvider provider = new(loader); + Mock> loggerValidator = new(); + RuntimeConfigValidator validator = new(provider, fileSystem, loggerValidator.Object); DataApiBuilderException exception = - Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, fileSystem)); + Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, validator, fileSystem)); Assert.AreEqual("The entity 'Character' was not found in the runtime config.", exception.Message); Assert.AreEqual(HttpStatusCode.ServiceUnavailable, exception.StatusCode); Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, exception.SubStatusCode); @@ -5288,6 +5293,12 @@ public async Task TestGraphQLIntrospectionQueriesAreNotImpactedByDepthLimit() } } + /// + /// + /// + /// + /// + /// [TestCategory(TestCategory.MSSQL)] [DataTestMethod] [DataRow(true, 4, DisplayName = "Test Autoentities with additional entities")] @@ -5434,6 +5445,115 @@ public async Task TestAutoentitiesAreGeneratedIntoEntities(bool useEntities, int } } + /// + /// + /// + /// + /// + /// + /// + /// + /// + [TestCategory(TestCategory.MSSQL)] + [DataTestMethod] + [DataRow("publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity with name 'publishers' already exists. Cannot create new entity from autoentity pattern with definition-name 'PublisherAutoEntity'.", DisplayName = "Autoentities fail due to entity name")] + [DataRow("UniquePublisher", "publishers", "uniquePluralPublishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql singular type")] + [DataRow("UniquePublisher", "uniqueSingularPublisher", "publishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql plural type")] + [DataRow("UniquePublisher", "uniqueSingularPublisher", "uniquePluralPublishers", "/publishers", "The rest path: publishers specified for entity: publishers is already used by another entity.", DisplayName = "Autoentities fail due to rest path")] + public async Task ValidateAutoentityGenerationConflicts(string entityName, string singular, string plural, string path, string exceptionMessage) + { + // Arrange + Entity publisherEntity = new( + Source: new("publishers", EntitySourceType.Table, null, null), + Fields: null, + Rest: new(Path: path), + GraphQL: new(Singular: singular, Plural: plural), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Dictionary entityMap = new() + { + { entityName, publisherEntity } + }; + + Dictionary autoentityMap = new() + { + { + "PublisherAutoEntity", new Autoentity( + Patterns: new AutoentityPatterns( + Include: new[] { "%publishers%" }, + Exclude: null, + Name: null + ), + Template: new AutoentityTemplate( + Rest: new EntityRestOptions( + Enabled: true), + GraphQL: new EntityGraphQLOptions( + Singular: string.Empty, + Plural: string.Empty, + Enabled: true + ), + Health: null, + Cache: null + ), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) } + ) + } + }; + + // Create DataSource for MSSQL connection + DataSource dataSource = new(DatabaseType.MSSQL, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); + + // Build complete runtime configuration with autoentities + RuntimeConfig configuration = new( + Schema: "TestAutoentitiesSchema", + DataSource: dataSource, + Runtime: new( + Rest: new(Enabled: true), + GraphQL: new(Enabled: true), + Mcp: new(Enabled: false), + Host: new( + Mode: HostMode.Development, + Cors: null, + Authentication: new Config.ObjectModel.AuthenticationOptions( + Provider: nameof(EasyAuthType.StaticWebApps), + Jwt: null + ) + ) + ), + Entities: new(entityMap), + Autoentities: new RuntimeAutoentities(autoentityMap) + ); + + File.WriteAllText(CUSTOM_CONFIG_FILENAME, configuration.ToJson()); + + IFileSystem fileSystem = new FileSystem(); + + FileSystemRuntimeConfigLoader configLoader = new(fileSystem) + { + RuntimeConfig = configuration + }; + + RuntimeConfigProvider configProvider = new(configLoader); + + Mock> loggerValidator = new(); + RuntimeConfigValidator configValidator = new(configProvider, fileSystem, loggerValidator.Object); + + LoggerFactory loggerFactory = new(); + + try + { + await configValidator.ValidateEntitiesMetadata(configuration, loggerFactory); + Assert.Fail("It is expected for DAB to fail due to entities not containing unique parameters."); + } + catch (Exception ex) + { + Assert.AreEqual(exceptionMessage, ex.Message); + } + } + /// /// 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. diff --git a/src/Service.Tests/CosmosTests/TestBase.cs b/src/Service.Tests/CosmosTests/TestBase.cs index 8617534776..dfac601023 100644 --- a/src/Service.Tests/CosmosTests/TestBase.cs +++ b/src/Service.Tests/CosmosTests/TestBase.cs @@ -21,6 +21,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json.Linq; @@ -158,7 +159,10 @@ protected WebApplicationFactory SetupTestApplicationFactory() FileSystemRuntimeConfigLoader loader = new(fileSystem); RuntimeConfigProvider provider = new(loader); - ISqlMetadataProvider cosmosSqlMetadataProvider = new CosmosSqlMetadataProvider(provider, fileSystem); + Mock> loggerValidator = new(); + RuntimeConfigValidator validator = new(provider, fileSystem, loggerValidator.Object); + + ISqlMetadataProvider cosmosSqlMetadataProvider = new CosmosSqlMetadataProvider(provider, validator, fileSystem); Mock metadataProviderFactory = new(); metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny())).Returns(cosmosSqlMetadataProvider); diff --git a/src/Service.Tests/GraphQLBuilder/MultiSourceBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MultiSourceBuilderTests.cs index 7c40dafcd6..67bfa8d2fe 100644 --- a/src/Service.Tests/GraphQLBuilder/MultiSourceBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MultiSourceBuilderTests.cs @@ -46,11 +46,14 @@ public async Task CosmosSchemaBuilderTestAsync() RuntimeConfigProvider provider = new(loader); + Mock> loggerValidator = new(); + RuntimeConfigValidator validator = new(provider, fs, loggerValidator.Object); + Mock queryManagerfactory = new(); Mock queryEngineFactory = new(); Mock mutationEngineFactory = new(); Mock> logger = new(); - IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, queryManagerfactory.Object, logger.Object, fs, handler: null); + IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, validator, queryManagerfactory.Object, logger.Object, fs, handler: null); Mock authResolver = new(); GraphQLSchemaCreator creator = new(provider, queryEngineFactory.Object, mutationEngineFactory.Object, metadataProviderFactory, authResolver.Object); diff --git a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs index 94665d7c18..ffe636f6db 100644 --- a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using Azure.DataApiBuilder.Auth; @@ -392,6 +393,16 @@ private static async Task GetGQLSchemaCreator(RuntimeConfi Mock cache = new(); DabCacheService cacheService = new(cache: cache.Object, logger: null, httpContextAccessor: httpContextAccessor.Object); + // Setup runtime config validator + IFileSystem fileSystem = new FileSystem(); + FileSystemRuntimeConfigLoader configLoader = new(fileSystem) + { + RuntimeConfig = _runtimeConfig + }; + RuntimeConfigProvider configProvider = new(configLoader); + Mock> loggerValidator = new(); + RuntimeConfigValidator configValidator = new(configProvider, fileSystem, loggerValidator.Object); + // Setup query manager factory. IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory( runtimeConfigProvider: runtimeConfigProvider, @@ -402,6 +413,7 @@ private static async Task GetGQLSchemaCreator(RuntimeConfi // Setup metadata provider factory. IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory( runtimeConfigProvider: runtimeConfigProvider, + runtimeConfigValidator: configValidator, queryManagerFactory: queryManagerfactory, logger: metadatProviderLogger.Object, fileSystem: null, diff --git a/src/Service.Tests/SqlTests/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs index 16e804f117..4e0fa5249c 100644 --- a/src/Service.Tests/SqlTests/SqlTestBase.cs +++ b/src/Service.Tests/SqlTests/SqlTestBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Net; using System.Net.Http; using System.Net.Http.Json; @@ -279,6 +280,10 @@ protected static void SetUpSQLMetadataProvider(RuntimeConfigProvider runtimeConf _queryManagerFactory = new Mock(); Mock httpContextAccessor = new(); string dataSourceName = runtimeConfigProvider.GetConfig().DefaultDataSourceName; + IFileSystem fileSystem = new FileSystem(); + Mock> loggerValidator = new(); + RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, loggerValidator.Object); + switch (DatabaseEngine) { case TestCategory.POSTGRESQL: @@ -297,6 +302,7 @@ protected static void SetUpSQLMetadataProvider(RuntimeConfigProvider runtimeConf _sqlMetadataProvider = new PostgreSqlMetadataProvider( runtimeConfigProvider, + runtimeConfigValidator, _queryManagerFactory.Object, _sqlMetadataLogger, dataSourceName); @@ -317,6 +323,7 @@ protected static void SetUpSQLMetadataProvider(RuntimeConfigProvider runtimeConf _sqlMetadataProvider = new MsSqlMetadataProvider( runtimeConfigProvider, + runtimeConfigValidator, _queryManagerFactory.Object, _sqlMetadataLogger, dataSourceName); @@ -337,6 +344,7 @@ protected static void SetUpSQLMetadataProvider(RuntimeConfigProvider runtimeConf _sqlMetadataProvider = new MySqlMetadataProvider( runtimeConfigProvider, + runtimeConfigValidator, _queryManagerFactory.Object, _sqlMetadataLogger, dataSourceName); @@ -357,6 +365,7 @@ protected static void SetUpSQLMetadataProvider(RuntimeConfigProvider runtimeConf _sqlMetadataProvider = new MsSqlMetadataProvider( runtimeConfigProvider, + runtimeConfigValidator, _queryManagerFactory.Object, _sqlMetadataLogger, dataSourceName); diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs index c9ce82180b..866e232afe 100644 --- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Data.Common; using System.IO; +using System.IO.Abstractions; using System.Net; using System.Text.Json.Nodes; using System.Threading.Tasks; @@ -129,8 +130,13 @@ public void CheckTablePrefix(string schemaName, string tableName, string expecte queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny())).Returns(queryBuilder); queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny())).Returns(queryExecutor.Object); + IFileSystem fileSystem = new FileSystem(); + Mock> loggerValidator = new(); + RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, loggerValidator.Object); + SqlMetadataProvider provider = new MsSqlMetadataProvider( runtimeConfigProvider, + runtimeConfigValidator, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName); @@ -215,11 +221,15 @@ private static async Task CheckExceptionForBadConnectionStringHelperAsync(string queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny())).Returns(_queryBuilder); queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny())).Returns(_queryExecutor); + IFileSystem fileSystem = new FileSystem(); + Mock> loggerValidator = new(); + RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, loggerValidator.Object); + ISqlMetadataProvider sqlMetadataProvider = databaseType switch { - TestCategory.MSSQL => new MsSqlMetadataProvider(runtimeConfigProvider, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName), - TestCategory.MYSQL => new MySqlMetadataProvider(runtimeConfigProvider, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName), - TestCategory.POSTGRESQL => new PostgreSqlMetadataProvider(runtimeConfigProvider, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName), + TestCategory.MSSQL => new MsSqlMetadataProvider(runtimeConfigProvider, runtimeConfigValidator, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName), + TestCategory.MYSQL => new MySqlMetadataProvider(runtimeConfigProvider, runtimeConfigValidator, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName), + TestCategory.POSTGRESQL => new PostgreSqlMetadataProvider(runtimeConfigProvider, runtimeConfigValidator, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName), _ => throw new ArgumentException($"Invalid database type: {databaseType}") }; @@ -480,8 +490,13 @@ public async Task ValidateExceptionForInvalidResultFieldNames(string invalidFiel queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny())).Returns(_queryBuilder); queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny())).Returns(mockQueryExecutor.Object); + IFileSystem fileSystem = new FileSystem(); + Mock> loggerValidator = new(); + RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, loggerValidator.Object); + ISqlMetadataProvider sqlMetadataProvider = new MsSqlMetadataProvider( runtimeConfigProvider, + runtimeConfigValidator, queryManagerFactory.Object, sqlMetadataLogger, dataSourceName); @@ -598,7 +613,7 @@ private static async Task SetupTestFixtureAndInferMetadata() [DataRow(new string[] { "dbo.%publish%" }, new string[] { }, "{schema}.{object}", new string[] { "publish" }, "")] [DataRow(new string[] { "dbo.%book%" }, new string[] { "dbo.%books%" }, "{schema}_{object}_exclude_books", new string[] { "book" }, "books")] [DataRow(new string[] { "dbo.%book%", "dbo.%publish%" }, new string[] { }, "{object}", new string[] { "book", "publish" }, "")] - [DataRow(new string[] { }, new string[] { "dbo.%book%" }, "{object}", new string[] { "" }, "book")] + [DataRow(new string[] { }, new string[] { "dbo.%book%" }, "{object}s", new string[] { "" }, "book")] public async Task CheckAutoentitiesQuery(string[] include, string[] exclude, string name, string[] includeObject, string excludeObject) { // Arrange diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index f9e7043440..c24d8be8a8 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -1174,16 +1174,8 @@ private async Task PerformOnConfigChangeAsync(IApplicationBuilder app) // Now that the configuration has been set, perform validation of the runtime config // itself. - // TODO: Task #3131. Need to change validation so that the validation of entities is done after the autoentities are generated and added with the regular entitites. runtimeConfigValidator.ValidateConfigProperties(); - if (runtimeConfig.IsDevelopmentMode()) - { - // Running only in developer mode to ensure fast and smooth startup in production. - // TODO: Task #3131. Need to change validation so that the validation of entities is done after the autoentities are generated and added with the regular entitites. - runtimeConfigValidator.ValidatePermissionsInConfig(runtimeConfig); - } - IMetadataProviderFactory sqlMetadataProviderFactory = app.ApplicationServices.GetRequiredService(); await sqlMetadataProviderFactory.InitializeAsync();