diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index d6bdefc1b8..301470b84c 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -1,878 +1 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System.Diagnostics.CodeAnalysis;
-using System.IO.Abstractions;
-using System.Net;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using Azure.DataApiBuilder.Config.Converters;
-using Azure.DataApiBuilder.Service.Exceptions;
-using Microsoft.Extensions.Logging;
-
-namespace Azure.DataApiBuilder.Config.ObjectModel;
-
-public record RuntimeConfig
-{
- [JsonPropertyName("$schema")]
- public string Schema { get; init; }
-
- public const string DEFAULT_CONFIG_SCHEMA_LINK = "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json";
-
- public DataSource DataSource { get; init; }
-
- public RuntimeOptions? Runtime { get; init; }
-
- [JsonPropertyName("azure-key-vault")]
- public AzureKeyVaultOptions? AzureKeyVault { get; init; }
-
- public RuntimeAutoentities Autoentities { get; init; }
-
- public virtual RuntimeEntities Entities { get; init; }
-
- public DataSourceFiles? DataSourceFiles { get; init; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
- public bool CosmosDataSourceUsed { get; private set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
- public bool SqlDataSourceUsed { get; private set; }
-
- ///
- /// Retrieves the value of runtime.CacheEnabled property if present, default is false.
- /// Caching is enabled only when explicitly set to true.
- ///
- /// Whether caching is globally enabled.
- [JsonIgnore]
- public bool IsCachingEnabled =>
- Runtime is not null &&
- Runtime.IsCachingEnabled;
-
- ///
- /// Retrieves the value of runtime.rest.request-body-strict property if present, default is true.
- ///
- [JsonIgnore]
- public bool IsRequestBodyStrict =>
- Runtime is null ||
- Runtime.Rest is null ||
- Runtime.Rest.RequestBodyStrict;
-
- ///
- /// Retrieves the value of runtime.graphql.enabled property if present, default is true.
- ///
- [JsonIgnore]
- public bool IsGraphQLEnabled => Runtime is null ||
- Runtime.GraphQL is null ||
- Runtime.GraphQL.Enabled;
-
- ///
- /// Retrieves the value of runtime.rest.enabled property if present, default is true if its not cosmosdb.
- ///
- [JsonIgnore]
- public bool IsRestEnabled =>
- (Runtime is null ||
- Runtime.Rest is null ||
- Runtime.Rest.Enabled) &&
- DataSource.DatabaseType != DatabaseType.CosmosDB_NoSQL;
-
- ///
- /// Retrieves the value of runtime.mcp.enabled property if present, default is true.
- ///
- [JsonIgnore]
- public bool IsMcpEnabled =>
- Runtime is null ||
- Runtime.Mcp is null ||
- Runtime.Mcp.Enabled;
-
- [JsonIgnore]
- public bool IsHealthEnabled =>
- Runtime is null ||
- Runtime.Health is null ||
- Runtime.Health.Enabled;
-
- ///
- /// A shorthand method to determine whether Static Web Apps is configured for the current authentication provider.
- ///
- /// True if the authentication provider is enabled for Static Web Apps, otherwise false.
- [JsonIgnore]
- public bool IsStaticWebAppsIdentityProvider =>
- Runtime?.Host?.Authentication is not null &&
- EasyAuthType.StaticWebApps.ToString().Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase);
-
- ///
- /// A shorthand method to determine whether App Service is configured for the current authentication provider.
- ///
- /// True if the authentication provider is enabled for App Service, otherwise false.
- [JsonIgnore]
- public bool IsAppServiceIdentityProvider =>
- Runtime?.Host?.Authentication is not null &&
- EasyAuthType.AppService.ToString().Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase);
-
- ///
- /// A shorthand method to determine whether Unauthenticated is configured for the current authentication provider.
- ///
- /// True if the authentication provider is Unauthenticated (the default), otherwise false.
- [JsonIgnore]
- public bool IsUnauthenticatedIdentityProvider =>
- Runtime is null ||
- Runtime.Host is null ||
- Runtime.Host.Authentication is null ||
- AuthenticationOptions.UNAUTHENTICATED_AUTHENTICATION.Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase);
-
- ///
- /// The path at which Rest APIs are available
- ///
- [JsonIgnore]
- public string RestPath
- {
- get
- {
- if (Runtime is null || Runtime.Rest is null)
- {
- return RestRuntimeOptions.DEFAULT_PATH;
- }
- else
- {
- return Runtime.Rest.Path;
- }
- }
- }
-
- ///
- /// The path at which GraphQL API is available
- ///
- [JsonIgnore]
- public string GraphQLPath
- {
- get
- {
- if (Runtime is null || Runtime.GraphQL is null)
- {
- return GraphQLRuntimeOptions.DEFAULT_PATH;
- }
- else
- {
- return Runtime.GraphQL.Path;
- }
- }
- }
-
- ///
- /// The path at which MCP API is available
- ///
- [JsonIgnore]
- public string McpPath
- {
- get
- {
- if (Runtime is null || Runtime.Mcp is null || Runtime.Mcp.Path is null)
- {
- return McpRuntimeOptions.DEFAULT_PATH;
- }
- else
- {
- return Runtime.Mcp.Path;
- }
- }
- }
-
- ///
- /// Indicates whether introspection is allowed or not.
- ///
- [JsonIgnore]
- public bool AllowIntrospection
- {
- get
- {
- return Runtime is null ||
- Runtime.GraphQL is null ||
- Runtime.GraphQL.AllowIntrospection;
- }
- }
-
- [JsonIgnore]
- public string DefaultDataSourceName { get; set; }
-
- ///
- /// Retrieves the value of runtime.graphql.aggregation.enabled property if present, default is true.
- ///
- [JsonIgnore]
- public bool EnableAggregation =>
- Runtime is not null &&
- Runtime.GraphQL is not null &&
- Runtime.GraphQL.EnableAggregation;
-
- [JsonIgnore]
- public HashSet AllowedRolesForHealth =>
- Runtime?.Health?.Roles ?? new HashSet();
-
- [JsonIgnore]
- public int CacheTtlSecondsForHealthReport =>
- Runtime?.Health?.CacheTtlSeconds ?? EntityCacheOptions.DEFAULT_TTL_SECONDS;
-
- ///
- /// Retrieves the value of runtime.graphql.dwnto1joinopt.enabled property if present, default is false.
- ///
- [JsonIgnore]
- public bool EnableDwNto1JoinOpt =>
- Runtime is not null &&
- Runtime.GraphQL is not null &&
- Runtime.GraphQL.FeatureFlags is not null &&
- Runtime.GraphQL.FeatureFlags.EnableDwNto1JoinQueryOptimization;
-
- private Dictionary _dataSourceNameToDataSource;
-
- private Dictionary _entityNameToDataSourceName = new();
-
- private Dictionary _autoentityNameToDataSourceName = new();
-
- private Dictionary _entityPathNameToEntityName = new();
-
- ///
- /// List of all datasources.
- ///
- /// List of datasources
- public IEnumerable ListAllDataSources()
- {
- return _dataSourceNameToDataSource.Values;
- }
-
- ///
- /// Get Iterator to iterate over dictionary.
- ///
- public IEnumerable> GetDataSourceNamesToDataSourcesIterator()
- {
- return _dataSourceNameToDataSource.AsEnumerable();
- }
-
- public bool TryAddEntityPathNameToEntityName(string entityPathName, string entityName)
- {
- return _entityPathNameToEntityName.TryAdd(entityPathName, entityName);
- }
-
- public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)] out string? entityName)
- {
- return _entityPathNameToEntityName.TryGetValue(entityPathName, out entityName);
- }
-
- public bool TryAddEntityNameToDataSourceName(string entityName)
- {
- return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName);
- }
-
- public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, string autoEntityDefinition)
- {
- if (_autoentityNameToDataSourceName.TryGetValue(autoEntityDefinition, out string? dataSourceName))
- {
- return _entityNameToDataSourceName.TryAdd(entityName, dataSourceName);
- }
-
- return false;
- }
-
- public bool RemoveGeneratedAutoentityNameFromDataSourceName(string entityName)
- {
- return _entityNameToDataSourceName.Remove(entityName);
- }
-
- ///
- /// Constructor for runtimeConfig.
- /// To be used when setting up from cli json scenario.
- ///
- /// schema for config.
- /// Default datasource.
- /// Entities
- /// Runtime settings.
- /// List of datasource files for multiple db scenario. Null for single db scenario.
- [JsonConstructor]
- public RuntimeConfig(
- string? Schema,
- DataSource DataSource,
- RuntimeEntities Entities,
- RuntimeAutoentities? Autoentities = null,
- RuntimeOptions? Runtime = null,
- DataSourceFiles? DataSourceFiles = null,
- AzureKeyVaultOptions? AzureKeyVault = null)
- {
- this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK;
- this.DataSource = DataSource;
- this.Runtime = Runtime;
- this.AzureKeyVault = AzureKeyVault;
- this.Entities = Entities ?? new RuntimeEntities(new Dictionary());
- this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary());
- this.DefaultDataSourceName = Guid.NewGuid().ToString();
-
- if (this.DataSource 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
- {
- { this.DefaultDataSourceName, this.DataSource }
- };
-
- _entityNameToDataSourceName = new Dictionary();
- if (Entities is null && this.Entities.Entities.Count == 0 &&
- Autoentities is null && this.Autoentities.Autoentities.Count == 0)
- {
- throw new DataApiBuilderException(
- message: "Configuration file should contain either at least the entities or autoentities property",
- statusCode: HttpStatusCode.UnprocessableEntity,
- subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
- }
-
- if (Entities is not null)
- {
- foreach (KeyValuePair entity in Entities)
- {
- _entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName);
- }
- }
-
- if (Autoentities is not null)
- {
- foreach (KeyValuePair autoentity in Autoentities)
- {
- _autoentityNameToDataSourceName.TryAdd(autoentity.Key, this.DefaultDataSourceName);
- }
- }
-
- // Process data source and entities information for each database in multiple database scenario.
- this.DataSourceFiles = DataSourceFiles;
-
- if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null)
- {
- IEnumerable>? allEntities = Entities?.AsEnumerable();
- IEnumerable>? 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);
-
- foreach (string dataSourceFile in DataSourceFiles.SourceFiles)
- {
- // Use Ignore mode so missing env vars are left as literal @env() strings,
- // consistent with how the parent config is loaded in TryLoadKnownConfig.
- DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true, envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore);
-
- if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings))
- {
- try
- {
- _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());
- }
- catch (Exception e)
- {
- // Errors could include duplicate datasource names, duplicate entity names, etc.
- throw new DataApiBuilderException(
- $"Error while loading datasource file {dataSourceFile} with exception {e.Message}",
- HttpStatusCode.ServiceUnavailable,
- DataApiBuilderException.SubStatusCodes.ConfigValidationError,
- e.InnerException);
- }
- }
- else if (fileSystem.File.Exists(dataSourceFile))
- {
- // The file exists but failed to load (e.g. invalid JSON, deserialization error).
- // Throw to prevent silently skipping a broken child config.
- // Non-existent files are skipped gracefully to support late-configured scenarios
- // where data-source-files may reference files not present on the host.
- throw new DataApiBuilderException(
- message: $"Failed to load datasource file: {dataSourceFile}. Ensure the file is accessible and contains a valid DAB configuration.",
- statusCode: HttpStatusCode.ServiceUnavailable,
- subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
- }
- }
-
- this.Entities = new RuntimeEntities(allEntities != null ? allEntities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary());
- this.Autoentities = new RuntimeAutoentities(allAutoentities != null ? allAutoentities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary());
- }
-
- SetupDataSourcesUsed();
- }
-
- ///
- /// Constructor for runtimeConfig.
- /// This constructor is to be used when dynamically setting up the config as opposed to using a cli json file.
- ///
- /// schema for config.
- /// Default datasource.
- /// Runtime settings.
- /// Entities
- /// Autoentities
- /// List of datasource files for multiple db scenario.Null for single db scenario.
- /// DefaultDataSourceName to maintain backward compatibility.
- /// Dictionary mapping datasourceName to datasource object.
- /// Dictionary mapping entityName to datasourceName.
- /// Datasource files which represent list of child runtimeconfigs for multi-db scenario.
- public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtime, RuntimeEntities Entities, string DefaultDataSourceName, Dictionary DataSourceNameToDataSource, Dictionary EntityNameToDataSourceName, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null, RuntimeAutoentities? Autoentities = null)
- {
- this.Schema = Schema;
- this.DataSource = DataSource;
- this.Runtime = Runtime;
- this.Entities = Entities;
- this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary());
- this.DefaultDataSourceName = DefaultDataSourceName;
- _dataSourceNameToDataSource = DataSourceNameToDataSource;
- _entityNameToDataSourceName = EntityNameToDataSourceName;
- this.DataSourceFiles = DataSourceFiles;
- this.AzureKeyVault = AzureKeyVault;
-
- SetupDataSourcesUsed();
- }
-
- ///
- /// Gets the DataSource corresponding to the datasourceName.
- ///
- /// Name of datasource.
- /// DataSource object.
- /// Not found exception if key is not found.
- public virtual DataSource GetDataSourceFromDataSourceName(string dataSourceName)
- {
- CheckDataSourceNamePresent(dataSourceName);
- return _dataSourceNameToDataSource[dataSourceName];
- }
-
- ///
- /// Updates the DataSourceNameToDataSource dictionary with the new datasource.
- ///
- /// Name of datasource
- /// Updated datasource value.
- /// Not found exception if key is not found.
- public void UpdateDataSourceNameToDataSource(string dataSourceName, DataSource dataSource)
- {
- CheckDataSourceNamePresent(dataSourceName);
- _dataSourceNameToDataSource[dataSourceName] = dataSource;
- }
-
- ///
- /// In a Hot Reload scenario we should maintain the same default data source
- /// name before the hot reload as after the hot reload. This is because we hold
- /// references to the Data Source itself which depend on this data source name
- /// for lookups. To correctly retrieve this information after a hot reload
- /// we need the data source name to stay the same after hot reloading. This method takes
- /// a default data source name, such as the one from before hot reload, and
- /// replaces the current dictionary entries of this RuntimeConfig that were
- /// built using a new, unique guid during the construction of this RuntimeConfig
- /// with entries using the provided default data source name. We then update the DefaultDataSourceName.
- ///
- /// The name used to update the dictionaries.
- public void UpdateDefaultDataSourceName(string initialDefaultDataSourceName)
- {
- _dataSourceNameToDataSource.Remove(DefaultDataSourceName);
- if (!_dataSourceNameToDataSource.TryAdd(initialDefaultDataSourceName, this.DataSource))
- {
- // An exception here means that a default data source name was generated as a GUID that
- // matches the original default data source name. This should never happen but we add this
- // to be extra safe.
- throw new DataApiBuilderException(
- message: $"Duplicate data source name: {initialDefaultDataSourceName}.",
- statusCode: HttpStatusCode.InternalServerError,
- subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
- }
-
- foreach (KeyValuePair entity in Entities)
- {
- _entityNameToDataSourceName[entity.Key] = initialDefaultDataSourceName;
- }
-
- DefaultDataSourceName = initialDefaultDataSourceName;
- }
-
- ///
- /// Gets datasourceName from EntityNameToDatasourceName dictionary.
- ///
- /// entityName
- /// DataSourceName
- public string GetDataSourceNameFromEntityName(string entityName)
- {
- CheckEntityNamePresent(entityName);
- return _entityNameToDataSourceName[entityName];
- }
-
- ///
- /// Gets datasource using entityName.
- ///
- /// entityName.
- /// DataSource using EntityName.
- public DataSource GetDataSourceFromEntityName(string entityName)
- {
- CheckEntityNamePresent(entityName);
- return _dataSourceNameToDataSource[_entityNameToDataSourceName[entityName]];
- }
-
- ///
- /// Gets datasourceName from AutoentityNameToDatasourceName dictionary.
- ///
- /// autoentityName
- /// DataSourceName
- public string GetDataSourceNameFromAutoentityName(string autoentityName)
- {
- if (!_autoentityNameToDataSourceName.TryGetValue(autoentityName, out string? autoentityDataSource))
- {
- throw new DataApiBuilderException(
- message: $"'{autoentityName}' is not a valid autoentities definition.",
- statusCode: HttpStatusCode.NotFound,
- subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
- }
-
- return autoentityDataSource;
- }
-
- ///
- /// Validates if datasource is present in runtimeConfig.
- ///
- public bool CheckDataSourceExists(string dataSourceName)
- {
- return _dataSourceNameToDataSource.ContainsKey(dataSourceName);
- }
-
- ///
- /// Serializes the RuntimeConfig object to JSON for writing to file.
- ///
- ///
- public string ToJson(JsonSerializerOptions? jsonSerializerOptions = null)
- {
- // get default serializer options if none provided.
- jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(replacementSettings: null);
- return JsonSerializer.Serialize(this, jsonSerializerOptions);
- }
-
- public bool IsDevelopmentMode() =>
- Runtime is not null && Runtime.Host is not null
- && Runtime.Host.Mode is HostMode.Development;
-
- ///
- /// Returns the ttl-seconds value for a given entity.
- /// If the entity explicitly sets ttl-seconds, that value is used.
- /// Otherwise, falls back to the global cache TTL setting.
- /// Callers are responsible for checking whether caching is enabled before using the result.
- ///
- /// Name of the entity to check cache configuration.
- /// Number of seconds (ttl) that a cache entry should be valid before cache eviction.
- /// Raised when an invalid entity name is provided.
- public virtual int GetEntityCacheEntryTtl(string entityName)
- {
- if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
- {
- throw new DataApiBuilderException(
- message: $"{entityName} is not a valid entity.",
- statusCode: HttpStatusCode.BadRequest,
- subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
- }
-
- if (entityConfig.Cache is not null && entityConfig.Cache.UserProvidedTtlOptions)
- {
- return entityConfig.Cache.TtlSeconds.Value;
- }
-
- return GlobalCacheEntryTtl();
- }
-
- ///
- /// Returns the cache level value for a given entity.
- /// If the entity explicitly sets level, that value is used.
- /// Otherwise, falls back to the global cache level or the default.
- /// Callers are responsible for checking whether caching is enabled before using the result.
- ///
- /// Name of the entity to check cache configuration.
- /// Cache level that a cache entry should be stored in.
- /// Raised when an invalid entity name is provided.
- public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)
- {
- if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
- {
- throw new DataApiBuilderException(
- message: $"{entityName} is not a valid entity.",
- statusCode: HttpStatusCode.BadRequest,
- subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
- }
-
- if (entityConfig.Cache is not null && entityConfig.Cache.UserProvidedLevelOptions)
- {
- return entityConfig.Cache.Level.Value;
- }
-
- // GlobalCacheEntryLevel() returns null when runtime cache is not configured.
- // Default to L1 to match EntityCacheOptions.DEFAULT_LEVEL.
- return GlobalCacheEntryLevel() ?? EntityCacheOptions.DEFAULT_LEVEL;
- }
-
- ///
- /// Returns the ttl-seconds value for the global cache entry.
- /// If no value is explicitly set, returns the global default value.
- ///
- /// Number of seconds a cache entry should be valid before cache eviction.
- public virtual int GlobalCacheEntryTtl()
- {
- return Runtime is not null && Runtime.IsCachingEnabled && Runtime.Cache.UserProvidedTtlOptions
- ? Runtime.Cache.TtlSeconds.Value
- : EntityCacheOptions.DEFAULT_TTL_SECONDS;
- }
-
- ///
- /// Returns the cache level value for the global cache entry.
- /// The level is inferred from the runtime cache Level2 configuration:
- /// if Level2 is enabled, the level is L1L2; otherwise L1.
- /// Returns null when runtime cache is not configured.
- ///
- /// Cache level for a cache entry, or null if runtime cache is not configured.
- public virtual EntityCacheLevel? GlobalCacheEntryLevel()
- {
- return Runtime?.Cache?.InferredLevel;
- }
-
- ///
- /// Whether the caching service should be used for a given operation. This is determined by
- /// - whether caching is enabled globally
- /// - whether the datasource is SQL and session context is disabled.
- ///
- /// Whether cache operations should proceed.
- public virtual bool CanUseCache()
- {
- bool setSessionContextEnabled = DataSource.GetTypedOptions()?.SetSessionContext ?? true;
- return IsCachingEnabled && !setSessionContextEnabled;
- }
-
- private void CheckDataSourceNamePresent(string dataSourceName)
- {
- if (!_dataSourceNameToDataSource.ContainsKey(dataSourceName))
- {
- throw new DataApiBuilderException($"{nameof(dataSourceName)}:{dataSourceName} could not be found within the config", HttpStatusCode.BadRequest, DataApiBuilderException.SubStatusCodes.DataSourceNotFound);
- }
- }
-
- private void CheckEntityNamePresent(string entityName)
- {
- if (!_entityNameToDataSourceName.ContainsKey(entityName))
- {
- throw new DataApiBuilderException(
- message: $"{entityName} is not a valid entity.",
- statusCode: HttpStatusCode.NotFound,
- subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
- }
- }
-
- private void SetupDataSourcesUsed()
- {
- SqlDataSourceUsed = _dataSourceNameToDataSource.Values.Any
- (x => x.DatabaseType is DatabaseType.MSSQL || x.DatabaseType is DatabaseType.PostgreSQL || x.DatabaseType is DatabaseType.MySQL || x.DatabaseType is DatabaseType.DWSQL);
-
- CosmosDataSourceUsed = _dataSourceNameToDataSource.Values.Any
- (x => x.DatabaseType is DatabaseType.CosmosDB_NoSQL);
- }
-
- ///
- /// Handles the logic for determining if we are in a scenario where hot reload is possible.
- /// Hot reload is currently not available, and so this will always return false. When hot reload
- /// becomes an available feature this logic will change to reflect the correct state based on
- /// the state of the runtime config and any other relevant factors.
- ///
- /// True in a scenario that support hot reload, false otherwise.
- public static bool IsHotReloadable()
- {
- // always return false while hot reload is not an available feature.
- return false;
- }
-
- ///
- /// Helper method to check if multiple create option is supported and enabled.
- ///
- /// Returns true when
- /// 1. Multiple create operation is supported by the database type and
- /// 2. Multiple create operation is enabled in the runtime config.
- ///
- ///
- public bool IsMultipleCreateOperationEnabled()
- {
- return Enum.GetNames(typeof(MultipleCreateSupportingDatabaseType)).Any(x => x.Equals(DataSource.DatabaseType.ToString(), StringComparison.OrdinalIgnoreCase)) &&
- (Runtime is not null &&
- Runtime.GraphQL is not null &&
- Runtime.GraphQL.MultipleMutationOptions is not null &&
- Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions is not null &&
- Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions.Enabled);
- }
-
- public uint DefaultPageSize()
- {
- return (uint?)Runtime?.Pagination?.DefaultPageSize ?? PaginationOptions.DEFAULT_PAGE_SIZE;
- }
-
- public uint MaxPageSize()
- {
- return (uint?)Runtime?.Pagination?.MaxPageSize ?? PaginationOptions.MAX_PAGE_SIZE;
- }
-
- public bool NextLinkRelative()
- {
- return Runtime?.Pagination?.NextLinkRelative ?? false;
- }
-
- public int MaxResponseSizeMB()
- {
- return Runtime?.Host?.MaxResponseSizeMB ?? HostOptions.MAX_RESPONSE_LENGTH_DAB_ENGINE_MB;
- }
-
- public bool MaxResponseSizeLogicEnabled()
- {
- // If the user has provided a max response size, we should use new logic to enforce it.
- return Runtime?.Host?.UserProvidedMaxResponseSizeMB ?? false;
- }
-
- ///
- /// Get the pagination limit from the runtime configuration.
- ///
- /// The pagination input from the user. Example: $first=10
- ///
- ///
- public uint GetPaginationLimit(int? first)
- {
- uint defaultPageSize = this.DefaultPageSize();
- uint maxPageSize = this.MaxPageSize();
-
- if (first is not null)
- {
- if (first < -1 || first == 0 || first > maxPageSize)
- {
- throw new DataApiBuilderException(
- message: $"Invalid number of items requested, {nameof(first)} argument must be either -1 or a positive number within the max page size limit of {maxPageSize}. Actual value: {first}",
- statusCode: HttpStatusCode.BadRequest,
- subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
- }
- else
- {
- return (first == -1 ? maxPageSize : (uint)first);
- }
- }
- else
- {
- return defaultPageSize;
- }
- }
-
- ///
- /// Checks if the property log-level or its value are null
- ///
- public bool IsLogLevelNull()
- {
- if (Runtime is null ||
- Runtime.Telemetry is null ||
- Runtime.Telemetry.LoggerLevel is null ||
- Runtime.Telemetry.LoggerLevel.Count == 0)
- {
- return true;
- }
-
- foreach (KeyValuePair logger in Runtime!.Telemetry.LoggerLevel)
- {
- if (logger.Key == null)
- {
- return true;
- }
- }
-
- return false;
- }
-
- ///
- /// Takes in the RuntimeConfig object and checks the LogLevel.
- /// If LogLevel is not null, it will return the current value as a LogLevel,
- /// else it will take the default option by checking host mode.
- /// If host mode is Development, return `LogLevel.Debug`, else
- /// for production returns `LogLevel.Error`.
- ///
- public LogLevel GetConfiguredLogLevel(string loggerFilter = "")
- {
- if (!IsLogLevelNull())
- {
- int max = 0;
- string currentFilter = string.Empty;
- foreach (KeyValuePair logger in Runtime!.Telemetry!.LoggerLevel!)
- {
- // Checks if the new key that is valid has more priority than the current key
- if (logger.Key.Length > max && loggerFilter.StartsWith(logger.Key))
- {
- max = logger.Key.Length;
- currentFilter = logger.Key;
- }
- }
-
- Runtime!.Telemetry!.LoggerLevel!.TryGetValue(currentFilter, out LogLevel? value);
- if (value is not null)
- {
- return (LogLevel)value;
- }
-
- value = Runtime!.Telemetry!.LoggerLevel!
- .SingleOrDefault(kvp => kvp.Key.Equals("default", StringComparison.OrdinalIgnoreCase)).Value;
- if (value is not null)
- {
- return (LogLevel)value;
- }
- }
-
- if (IsDevelopmentMode())
- {
- return LogLevel.Debug;
- }
-
- return LogLevel.Error;
- }
-
- ///
- /// Gets the MCP DML tools configuration
- ///
- [JsonIgnore]
- public DmlToolsConfig? McpDmlTools => Runtime?.Mcp?.DmlTools;
-
- ///
- /// Determines whether caching is enabled for a given entity, resolving inheritance lazily.
- /// If the entity explicitly sets cache enabled/disabled, that value wins.
- /// If the entity has a cache object but did not explicitly set enabled (UserProvidedEnabledOptions is false),
- /// the global runtime cache enabled setting is inherited.
- /// If the entity has no cache config at all, the global runtime cache enabled setting is inherited.
- ///
- /// Name of the entity to check cache configuration.
- /// Whether caching is enabled for the entity.
- /// Raised when an invalid entity name is provided.
- public virtual bool IsEntityCachingEnabled(string entityName)
- {
- if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
- {
- throw new DataApiBuilderException(
- message: $"{entityName} is not a valid entity.",
- statusCode: HttpStatusCode.BadRequest,
- subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
- }
-
- return IsEntityCachingEnabled(entityConfig);
- }
-
- ///
- /// Determines whether caching is enabled for a given entity, resolving inheritance lazily.
- /// If the entity explicitly sets cache enabled/disabled (UserProvidedEnabledOptions is true), that value wins.
- /// Otherwise, inherits the global runtime cache enabled setting.
- ///
- /// The entity to check cache configuration.
- /// Whether caching is enabled for the entity.
- private bool IsEntityCachingEnabled(Entity entity)
- {
- // If entity has an explicit cache config with user-provided enabled value, use it.
- if (entity.Cache is not null && entity.Cache.UserProvidedEnabledOptions)
- {
- return entity.IsCachingEnabled;
- }
-
- // Otherwise, inherit from the global runtime cache setting.
- return IsCachingEnabled;
- }
-}
+// Copyright (c) Microsoft Corporation.// Licensed under the MIT License.using System.Diagnostics.CodeAnalysis;using System.IO.Abstractions;using System.Net;using System.Text.Json;using System.Text.Json.Serialization;using Azure.DataApiBuilder.Config.Converters;using Azure.DataApiBuilder.Service.Exceptions;using Microsoft.Extensions.Logging;namespace Azure.DataApiBuilder.Config.ObjectModel;public record RuntimeConfig{ [JsonPropertyName("$schema")] public string Schema { get; init; } public const string DEFAULT_CONFIG_SCHEMA_LINK = "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"; public DataSource DataSource { get; init; } public RuntimeOptions? Runtime { get; init; } [JsonPropertyName("azure-key-vault")] public AzureKeyVaultOptions? AzureKeyVault { get; init; } public RuntimeAutoentities Autoentities { get; init; } public virtual RuntimeEntities Entities { get; init; } public DataSourceFiles? DataSourceFiles { get; init; } [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public bool CosmosDataSourceUsed { get; private set; } [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public bool SqlDataSourceUsed { get; private set; } /// /// Retrieves the value of runtime.CacheEnabled property if present, default is false. /// Caching is enabled only when explicitly set to true. /// /// Whether caching is globally enabled. [JsonIgnore] public bool IsCachingEnabled => Runtime is not null && Runtime.IsCachingEnabled; /// /// Retrieves the value of runtime.rest.request-body-strict property if present, default is true. /// [JsonIgnore] public bool IsRequestBodyStrict => Runtime is null || Runtime.Rest is null || Runtime.Rest.RequestBodyStrict; /// /// Retrieves the value of runtime.graphql.enabled property if present, default is true. /// [JsonIgnore] public bool IsGraphQLEnabled => Runtime is null || Runtime.GraphQL is null || Runtime.GraphQL.Enabled; /// /// Retrieves the value of runtime.rest.enabled property if present, default is true if its not cosmosdb. /// [JsonIgnore] public bool IsRestEnabled => (Runtime is null || Runtime.Rest is null || Runtime.Rest.Enabled) && DataSource.DatabaseType != DatabaseType.CosmosDB_NoSQL; /// /// Retrieves the value of runtime.mcp.enabled property if present, default is true. /// [JsonIgnore] public bool IsMcpEnabled => Runtime is null || Runtime.Mcp is null || Runtime.Mcp.Enabled; [JsonIgnore] public bool IsHealthEnabled => Runtime is null || Runtime.Health is null || Runtime.Health.Enabled; /// /// A shorthand method to determine whether Static Web Apps is configured for the current authentication provider. /// /// True if the authentication provider is enabled for Static Web Apps, otherwise false. [JsonIgnore] public bool IsStaticWebAppsIdentityProvider => Runtime?.Host?.Authentication is not null && EasyAuthType.StaticWebApps.ToString().Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase); /// /// A shorthand method to determine whether App Service is configured for the current authentication provider. /// /// True if the authentication provider is enabled for App Service, otherwise false. [JsonIgnore] public bool IsAppServiceIdentityProvider => Runtime?.Host?.Authentication is not null && EasyAuthType.AppService.ToString().Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase); /// /// A shorthand method to determine whether Unauthenticated is configured for the current authentication provider. /// /// True if the authentication provider is Unauthenticated (the default), otherwise false. [JsonIgnore] public bool IsUnauthenticatedIdentityProvider => Runtime is null || Runtime.Host is null || Runtime.Host.Authentication is null || AuthenticationOptions.UNAUTHENTICATED_AUTHENTICATION.Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase); /// /// The path at which Rest APIs are available /// [JsonIgnore] public string RestPath { get { if (Runtime is null || Runtime.Rest is null) { return RestRuntimeOptions.DEFAULT_PATH; } else { return Runtime.Rest.Path; } } } /// /// The path at which GraphQL API is available /// [JsonIgnore] public string GraphQLPath { get { if (Runtime is null || Runtime.GraphQL is null) { return GraphQLRuntimeOptions.DEFAULT_PATH; } else { return Runtime.GraphQL.Path; } } } /// /// The path at which MCP API is available /// [JsonIgnore] public string McpPath { get { if (Runtime is null || Runtime.Mcp is null || Runtime.Mcp.Path is null) { return McpRuntimeOptions.DEFAULT_PATH; } else { return Runtime.Mcp.Path; } } } /// /// Indicates whether introspection is allowed or not. /// [JsonIgnore] public bool AllowIntrospection { get { return Runtime is null || Runtime.GraphQL is null || Runtime.GraphQL.AllowIntrospection; } } [JsonIgnore] public string DefaultDataSourceName { get; set; } /// /// Retrieves the value of runtime.graphql.aggregation.enabled property if present, default is true. /// [JsonIgnore] public bool EnableAggregation => Runtime is not null && Runtime.GraphQL is not null && Runtime.GraphQL.EnableAggregation; [JsonIgnore] public HashSet AllowedRolesForHealth => Runtime?.Health?.Roles ?? new HashSet(); [JsonIgnore] public int CacheTtlSecondsForHealthReport => Runtime?.Health?.CacheTtlSeconds ?? EntityCacheOptions.DEFAULT_TTL_SECONDS; /// /// Retrieves the value of runtime.graphql.dwnto1joinopt.enabled property if present, default is false. /// [JsonIgnore] public bool EnableDwNto1JoinOpt => Runtime is not null && Runtime.GraphQL is not null && Runtime.GraphQL.FeatureFlags is not null && Runtime.GraphQL.FeatureFlags.EnableDwNto1JoinQueryOptimization; private Dictionary _dataSourceNameToDataSource; private Dictionary _entityNameToDataSourceName = new(); private Dictionary _autoentityNameToDataSourceName = new(); private Dictionary _entityPathNameToEntityName = new(); /// /// List of all datasources. /// /// List of datasources public IEnumerable ListAllDataSources() { return _dataSourceNameToDataSource.Values; } /// /// Get Iterator to iterate over dictionary. /// public IEnumerable> GetDataSourceNamesToDataSourcesIterator() { return _dataSourceNameToDataSource.AsEnumerable(); } public bool TryAddEntityPathNameToEntityName(string entityPathName, string entityName) { return _entityPathNameToEntityName.TryAdd(entityPathName, entityName); } public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)] out string? entityName) { return _entityPathNameToEntityName.TryGetValue(entityPathName, out entityName); } public bool TryAddEntityNameToDataSourceName(string entityName) { return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName); } public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, string autoEntityDefinition) { if (_autoentityNameToDataSourceName.TryGetValue(autoEntityDefinition, out string? dataSourceName)) { return _entityNameToDataSourceName.TryAdd(entityName, dataSourceName); } return false; } public bool RemoveGeneratedAutoentityNameFromDataSourceName(string entityName) { return _entityNameToDataSourceName.Remove(entityName); } /// /// Constructor for runtimeConfig. /// To be used when setting up from cli json scenario. /// /// schema for config. /// Default datasource. /// Entities /// Runtime settings. /// List of datasource files for multiple db scenario. Null for single db scenario. [JsonConstructor] public RuntimeConfig( string? Schema, DataSource DataSource, RuntimeEntities Entities, RuntimeAutoentities? Autoentities = null, RuntimeOptions? Runtime = null, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null) { this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK; this.DataSource = DataSource; this.Runtime = Runtime; this.AzureKeyVault = AzureKeyVault; this.Entities = Entities ?? new RuntimeEntities(new Dictionary()); this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary()); this.DefaultDataSourceName = Guid.NewGuid().ToString(); if (this.DataSource 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 { { this.DefaultDataSourceName, this.DataSource } }; _entityNameToDataSourceName = new Dictionary(); if (Entities is null && this.Entities.Entities.Count == 0 && Autoentities is null && this.Autoentities.Autoentities.Count == 0) { throw new DataApiBuilderException( message: "Configuration file should contain either at least the entities or autoentities property", statusCode: HttpStatusCode.UnprocessableEntity, subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } if (Entities is not null) { foreach (KeyValuePair entity in Entities) { _entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName); } } if (Autoentities is not null) { foreach (KeyValuePair autoentity in Autoentities) { _autoentityNameToDataSourceName.TryAdd(autoentity.Key, this.DefaultDataSourceName); } } // Process data source and entities information for each database in multiple database scenario. this.DataSourceFiles = DataSourceFiles; if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null) { IEnumerable>? allEntities = Entities?.AsEnumerable(); IEnumerable>? 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. // If a child config defines its own azure-key-vault section, TryParseConfig's // ExtractAzureKeyVaultOptions will detect it and override these parent options. DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: this.AzureKeyVault, doReplaceEnvVar: true, doReplaceAkvVar: true, envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore); foreach (string dataSourceFile in DataSourceFiles.SourceFiles) { if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings)) { try { _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()); } catch (Exception e) { // Errors could include duplicate datasource names, duplicate entity names, etc. throw new DataApiBuilderException( $"Error while loading datasource file {dataSourceFile} with exception {e.Message}", HttpStatusCode.ServiceUnavailable, DataApiBuilderException.SubStatusCodes.ConfigValidationError, e.InnerException); } } } this.Entities = new RuntimeEntities(allEntities != null ? allEntities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary()); this.Autoentities = new RuntimeAutoentities(allAutoentities != null ? allAutoentities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary()); } SetupDataSourcesUsed(); } /// /// Constructor for runtimeConfig. /// This constructor is to be used when dynamically setting up the config as opposed to using a cli json file. /// /// schema for config. /// Default datasource. /// Runtime settings. /// Entities /// Autoentities /// List of datasource files for multiple db scenario.Null for single db scenario. /// DefaultDataSourceName to maintain backward compatibility. /// Dictionary mapping datasourceName to datasource object. /// Dictionary mapping entityName to datasourceName. /// Datasource files which represent list of child runtimeconfigs for multi-db scenario. public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtime, RuntimeEntities Entities, string DefaultDataSourceName, Dictionary DataSourceNameToDataSource, Dictionary EntityNameToDataSourceName, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null, RuntimeAutoentities? Autoentities = null) { this.Schema = Schema; this.DataSource = DataSource; this.Runtime = Runtime; this.Entities = Entities; this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary()); this.DefaultDataSourceName = DefaultDataSourceName; _dataSourceNameToDataSource = DataSourceNameToDataSource; _entityNameToDataSourceName = EntityNameToDataSourceName; this.DataSourceFiles = DataSourceFiles; this.AzureKeyVault = AzureKeyVault; SetupDataSourcesUsed(); } /// /// Gets the DataSource corresponding to the datasourceName. /// /// Name of datasource. /// DataSource object. /// Not found exception if key is not found. public virtual DataSource GetDataSourceFromDataSourceName(string dataSourceName) { CheckDataSourceNamePresent(dataSourceName); return _dataSourceNameToDataSource[dataSourceName]; } /// /// Updates the DataSourceNameToDataSource dictionary with the new datasource. /// /// Name of datasource /// Updated datasource value. /// Not found exception if key is not found. public void UpdateDataSourceNameToDataSource(string dataSourceName, DataSource dataSource) { CheckDataSourceNamePresent(dataSourceName); _dataSourceNameToDataSource[dataSourceName] = dataSource; } /// /// In a Hot Reload scenario we should maintain the same default data source /// name before the hot reload as after the hot reload. This is because we hold /// references to the Data Source itself which depend on this data source name /// for lookups. To correctly retrieve this information after a hot reload /// we need the data source name to stay the same after hot reloading. This method takes /// a default data source name, such as the one from before hot reload, and /// replaces the current dictionary entries of this RuntimeConfig that were /// built using a new, unique guid during the construction of this RuntimeConfig /// with entries using the provided default data source name. We then update the DefaultDataSourceName. /// /// The name used to update the dictionaries. public void UpdateDefaultDataSourceName(string initialDefaultDataSourceName) { _dataSourceNameToDataSource.Remove(DefaultDataSourceName); if (!_dataSourceNameToDataSource.TryAdd(initialDefaultDataSourceName, this.DataSource)) { // An exception here means that a default data source name was generated as a GUID that // matches the original default data source name. This should never happen but we add this // to be extra safe. throw new DataApiBuilderException( message: $"Duplicate data source name: {initialDefaultDataSourceName}.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } foreach (KeyValuePair entity in Entities) { _entityNameToDataSourceName[entity.Key] = initialDefaultDataSourceName; } DefaultDataSourceName = initialDefaultDataSourceName; } /// /// Gets datasourceName from EntityNameToDatasourceName dictionary. /// /// entityName /// DataSourceName public string GetDataSourceNameFromEntityName(string entityName) { CheckEntityNamePresent(entityName); return _entityNameToDataSourceName[entityName]; } /// /// Gets datasource using entityName. /// /// entityName. /// DataSource using EntityName. public DataSource GetDataSourceFromEntityName(string entityName) { CheckEntityNamePresent(entityName); return _dataSourceNameToDataSource[_entityNameToDataSourceName[entityName]]; } /// /// Gets datasourceName from AutoentityNameToDatasourceName dictionary. /// /// autoentityName /// DataSourceName public string GetDataSourceNameFromAutoentityName(string autoentityName) { if (!_autoentityNameToDataSourceName.TryGetValue(autoentityName, out string? autoentityDataSource)) { throw new DataApiBuilderException( message: $"{autoentityName} is not a valid autoentity.", statusCode: HttpStatusCode.NotFound, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } return autoentityDataSource; } /// /// Validates if datasource is present in runtimeConfig. /// public bool CheckDataSourceExists(string dataSourceName) { return _dataSourceNameToDataSource.ContainsKey(dataSourceName); } /// /// Serializes the RuntimeConfig object to JSON for writing to file. /// /// public string ToJson(JsonSerializerOptions? jsonSerializerOptions = null) { // get default serializer options if none provided. jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(replacementSettings: null); return JsonSerializer.Serialize(this, jsonSerializerOptions); } public bool IsDevelopmentMode() => Runtime is not null && Runtime.Host is not null && Runtime.Host.Mode is HostMode.Development; /// /// Returns the ttl-seconds value for a given entity. /// If the entity explicitly sets ttl-seconds, that value is used. /// Otherwise, falls back to the global cache TTL setting. /// Callers are responsible for checking whether caching is enabled before using the result. /// /// Name of the entity to check cache configuration. /// Number of seconds (ttl) that a cache entry should be valid before cache eviction. /// Raised when an invalid entity name is provided. public virtual int GetEntityCacheEntryTtl(string entityName) { if (!Entities.TryGetValue(entityName, out Entity? entityConfig)) { throw new DataApiBuilderException( message: $"{entityName} is not a valid entity.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } if (entityConfig.Cache is not null && entityConfig.Cache.UserProvidedTtlOptions) { return entityConfig.Cache.TtlSeconds.Value; } return GlobalCacheEntryTtl(); } /// /// Returns the cache level value for a given entity. /// If the entity explicitly sets level, that value is used. /// Otherwise, falls back to the global cache level or the default. /// Callers are responsible for checking whether caching is enabled before using the result. /// /// Name of the entity to check cache configuration. /// Cache level that a cache entry should be stored in. /// Raised when an invalid entity name is provided. public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName) { if (!Entities.TryGetValue(entityName, out Entity? entityConfig)) { throw new DataApiBuilderException( message: $"{entityName} is not a valid entity.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } if (entityConfig.Cache is not null && entityConfig.Cache.UserProvidedLevelOptions) { return entityConfig.Cache.Level.Value; } // GlobalCacheEntryLevel() returns null when runtime cache is not configured. // Default to L1 to match EntityCacheOptions.DEFAULT_LEVEL. return GlobalCacheEntryLevel() ?? EntityCacheOptions.DEFAULT_LEVEL; } /// /// Returns the ttl-seconds value for the global cache entry. /// If no value is explicitly set, returns the global default value. /// /// Number of seconds a cache entry should be valid before cache eviction. public virtual int GlobalCacheEntryTtl() { return Runtime is not null && Runtime.IsCachingEnabled && Runtime.Cache.UserProvidedTtlOptions ? Runtime.Cache.TtlSeconds.Value : EntityCacheOptions.DEFAULT_TTL_SECONDS; } /// /// Returns the cache level value for the global cache entry. /// The level is inferred from the runtime cache Level2 configuration: /// if Level2 is enabled, the level is L1L2; otherwise L1. /// Returns null when runtime cache is not configured. /// /// Cache level for a cache entry, or null if runtime cache is not configured. public virtual EntityCacheLevel? GlobalCacheEntryLevel() { return Runtime?.Cache?.InferredLevel; } /// /// Whether the caching service should be used for a given operation. This is determined by /// - whether caching is enabled globally /// - whether the datasource is SQL and session context is disabled. /// /// Whether cache operations should proceed. public virtual bool CanUseCache() { bool setSessionContextEnabled = DataSource.GetTypedOptions()?.SetSessionContext ?? true; return IsCachingEnabled && !setSessionContextEnabled; } private void CheckDataSourceNamePresent(string dataSourceName) { if (!_dataSourceNameToDataSource.ContainsKey(dataSourceName)) { throw new DataApiBuilderException($"{nameof(dataSourceName)}:{dataSourceName} could not be found within the config", HttpStatusCode.BadRequest, DataApiBuilderException.SubStatusCodes.DataSourceNotFound); } } private void CheckEntityNamePresent(string entityName) { if (!_entityNameToDataSourceName.ContainsKey(entityName)) { throw new DataApiBuilderException( message: $"{entityName} is not a valid entity.", statusCode: HttpStatusCode.NotFound, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } } private void SetupDataSourcesUsed() { SqlDataSourceUsed = _dataSourceNameToDataSource.Values.Any (x => x.DatabaseType is DatabaseType.MSSQL || x.DatabaseType is DatabaseType.PostgreSQL || x.DatabaseType is DatabaseType.MySQL || x.DatabaseType is DatabaseType.DWSQL); CosmosDataSourceUsed = _dataSourceNameToDataSource.Values.Any (x => x.DatabaseType is DatabaseType.CosmosDB_NoSQL); } /// /// Handles the logic for determining if we are in a scenario where hot reload is possible. /// Hot reload is currently not available, and so this will always return false. When hot reload /// becomes an available feature this logic will change to reflect the correct state based on /// the state of the runtime config and any other relevant factors. /// /// True in a scenario that support hot reload, false otherwise. public static bool IsHotReloadable() { // always return false while hot reload is not an available feature. return false; } /// /// Helper method to check if multiple create option is supported and enabled. /// /// Returns true when /// 1. Multiple create operation is supported by the database type and /// 2. Multiple create operation is enabled in the runtime config. /// /// public bool IsMultipleCreateOperationEnabled() { return Enum.GetNames(typeof(MultipleCreateSupportingDatabaseType)).Any(x => x.Equals(DataSource.DatabaseType.ToString(), StringComparison.OrdinalIgnoreCase)) && (Runtime is not null && Runtime.GraphQL is not null && Runtime.GraphQL.MultipleMutationOptions is not null && Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions is not null && Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions.Enabled); } public uint DefaultPageSize() { return (uint?)Runtime?.Pagination?.DefaultPageSize ?? PaginationOptions.DEFAULT_PAGE_SIZE; } public uint MaxPageSize() { return (uint?)Runtime?.Pagination?.MaxPageSize ?? PaginationOptions.MAX_PAGE_SIZE; } public bool NextLinkRelative() { return Runtime?.Pagination?.NextLinkRelative ?? false; } public int MaxResponseSizeMB() { return Runtime?.Host?.MaxResponseSizeMB ?? HostOptions.MAX_RESPONSE_LENGTH_DAB_ENGINE_MB; } public bool MaxResponseSizeLogicEnabled() { // If the user has provided a max response size, we should use new logic to enforce it. return Runtime?.Host?.UserProvidedMaxResponseSizeMB ?? false; } /// /// Get the pagination limit from the runtime configuration. /// /// The pagination input from the user. Example: $first=10 /// /// public uint GetPaginationLimit(int? first) { uint defaultPageSize = this.DefaultPageSize(); uint maxPageSize = this.MaxPageSize(); if (first is not null) { if (first < -1 || first == 0 || first > maxPageSize) { throw new DataApiBuilderException( message: $"Invalid number of items requested, {nameof(first)} argument must be either -1 or a positive number within the max page size limit of {maxPageSize}. Actual value: {first}", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } else { return (first == -1 ? maxPageSize : (uint)first); } } else { return defaultPageSize; } } /// /// Checks if the property log-level or its value are null /// public bool IsLogLevelNull() { if (Runtime is null || Runtime.Telemetry is null || Runtime.Telemetry.LoggerLevel is null || Runtime.Telemetry.LoggerLevel.Count == 0) { return true; } foreach (KeyValuePair logger in Runtime!.Telemetry.LoggerLevel) { if (logger.Key == null) { return true; } } return false; } /// /// Takes in the RuntimeConfig object and checks the LogLevel. /// If LogLevel is not null, it will return the current value as a LogLevel, /// else it will take the default option by checking host mode. /// If host mode is Development, return `LogLevel.Debug`, else /// for production returns `LogLevel.Error`. /// public LogLevel GetConfiguredLogLevel(string loggerFilter = "") { if (!IsLogLevelNull()) { int max = 0; string currentFilter = string.Empty; foreach (KeyValuePair logger in Runtime!.Telemetry!.LoggerLevel!) { // Checks if the new key that is valid has more priority than the current key if (logger.Key.Length > max && loggerFilter.StartsWith(logger.Key)) { max = logger.Key.Length; currentFilter = logger.Key; } } Runtime!.Telemetry!.LoggerLevel!.TryGetValue(currentFilter, out LogLevel? value); if (value is not null) { return (LogLevel)value; } value = Runtime!.Telemetry!.LoggerLevel! .SingleOrDefault(kvp => kvp.Key.Equals("default", StringComparison.OrdinalIgnoreCase)).Value; if (value is not null) { return (LogLevel)value; } } if (IsDevelopmentMode()) { return LogLevel.Debug; } return LogLevel.Error; } /// /// Gets the MCP DML tools configuration /// [JsonIgnore] public DmlToolsConfig? McpDmlTools => Runtime?.Mcp?.DmlTools; /// /// Determines whether caching is enabled for a given entity, resolving inheritance lazily. /// If the entity explicitly sets cache enabled/disabled, that value wins. /// If the entity has a cache object but did not explicitly set enabled (UserProvidedEnabledOptions is false), /// the global runtime cache enabled setting is inherited. /// If the entity has no cache config at all, the global runtime cache enabled setting is inherited. /// /// Name of the entity to check cache configuration. /// Whether caching is enabled for the entity. /// Raised when an invalid entity name is provided. public virtual bool IsEntityCachingEnabled(string entityName) { if (!Entities.TryGetValue(entityName, out Entity? entityConfig)) { throw new DataApiBuilderException( message: $"{entityName} is not a valid entity.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } return IsEntityCachingEnabled(entityConfig); } /// /// Determines whether caching is enabled for a given entity, resolving inheritance lazily. /// If the entity explicitly sets cache enabled/disabled (UserProvidedEnabledOptions is true), that value wins. /// Otherwise, inherits the global runtime cache enabled setting. /// /// The entity to check cache configuration. /// Whether caching is enabled for the entity. private bool IsEntityCachingEnabled(Entity entity) { // If entity has an explicit cache config with user-provided enabled value, use it. if (entity.Cache is not null && entity.Cache.UserProvidedEnabledOptions) { return entity.IsCachingEnabled; } // Otherwise, inherit from the global runtime cache setting. return IsCachingEnabled; }}
\ No newline at end of file
diff --git a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
index 28b6fbb88d..a08bf1b8a1 100644
--- a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
+++ b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
@@ -133,6 +133,226 @@ public async Task CanLoadValidMultiSourceConfigWithAutoentities(string configPat
Assert.AreEqual(expectedEntities, runtimeConfig.Entities.Entities.Count, "Number of entities is not what is expected.");
}
+ ///
+ /// Validates that when a parent config has azure-key-vault options configured,
+ /// child configs can resolve @akv('...') references using the parent's AKV configuration.
+ /// Uses a local .akv file to simulate Azure Key Vault without requiring a real vault.
+ /// Regression test for https://github.com/Azure/data-api-builder/issues/3322
+ ///
+ [TestMethod]
+ public async Task ChildConfigResolvesAkvReferencesFromParentAkvOptions()
+ {
+ string akvFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".akv");
+ string childFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".json");
+
+ try
+ {
+ // Create a local .akv secrets file with test secrets.
+ await File.WriteAllTextAsync(akvFilePath, "my-connection-secret=Server=tcp:127.0.0.1,1433;Trusted_Connection=True;\n");
+
+ // Parent config with azure-key-vault pointing to the local .akv file.
+ string parentConfig = $@"{{
+ ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
+ ""data-source"": {{
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;""
+ }},
+ ""azure-key-vault"": {{
+ ""endpoint"": ""{akvFilePath.Replace("\\", "\\\\")}""
+ }},
+ ""data-source-files"": [""{childFilePath.Replace("\\", "\\\\")}""],
+ ""runtime"": {{
+ ""rest"": {{ ""enabled"": true }},
+ ""graphql"": {{ ""enabled"": true }},
+ ""host"": {{
+ ""cors"": {{ ""origins"": [] }},
+ ""authentication"": {{ ""provider"": ""StaticWebApps"" }}
+ }}
+ }},
+ ""entities"": {{}}
+ }}";
+
+ // Child config with @akv('...') reference in its connection string.
+ string childConfig = @"{
+ ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
+ ""data-source"": {
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""@akv('my-connection-secret')""
+ },
+ ""runtime"": {
+ ""rest"": { ""enabled"": true },
+ ""graphql"": { ""enabled"": true },
+ ""host"": {
+ ""cors"": { ""origins"": [] },
+ ""authentication"": { ""provider"": ""StaticWebApps"" }
+ }
+ },
+ ""entities"": {
+ ""AkvChildEntity"": {
+ ""source"": ""dbo.AkvTable"",
+ ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""read""] }]
+ }
+ }
+ }";
+
+ await File.WriteAllTextAsync(childFilePath, childConfig);
+
+ MockFileSystem fs = new(new Dictionary()
+ {
+ { "dab-config.json", new MockFileData(parentConfig) }
+ });
+
+ FileSystemRuntimeConfigLoader loader = new(fs);
+
+ DeserializationVariableReplacementSettings replacementSettings = new(
+ azureKeyVaultOptions: new AzureKeyVaultOptions() { Endpoint = akvFilePath, UserProvidedEndpoint = true },
+ doReplaceEnvVar: true,
+ doReplaceAkvVar: true,
+ envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore);
+
+ Assert.IsTrue(
+ loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig, replacementSettings: replacementSettings),
+ "Config should load successfully when child config has @akv() references resolvable via parent AKV options.");
+
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("AkvChildEntity"), "Child config entity should be merged into the parent config.");
+
+ // Verify the child's connection string was resolved from the .akv file.
+ string childDataSourceName = runtimeConfig.GetDataSourceNameFromEntityName("AkvChildEntity");
+ DataSource childDataSource = runtimeConfig.GetDataSourceFromDataSourceName(childDataSourceName);
+ Assert.IsTrue(
+ childDataSource.ConnectionString.Contains("127.0.0.1"),
+ "Child config connection string should have the AKV secret resolved.");
+ }
+ finally
+ {
+ if (File.Exists(akvFilePath))
+ {
+ File.Delete(akvFilePath);
+ }
+
+ if (File.Exists(childFilePath))
+ {
+ File.Delete(childFilePath);
+ }
+ }
+ }
+
+ ///
+ /// Validates that when both the parent and child configs define azure-key-vault options,
+ /// the child's AKV settings take precedence over the parent's.
+ /// The child config references a secret that only exists in the child's .akv file,
+ /// proving the child's AKV endpoint was used instead of the parent's.
+ ///
+ [TestMethod]
+ public async Task ChildAkvOptionsOverrideParentAkvOptions()
+ {
+ string parentAkvFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".akv");
+ string childAkvFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".akv");
+ string childFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".json");
+
+ try
+ {
+ // Parent's .akv file does NOT contain the secret the child references.
+ await File.WriteAllTextAsync(parentAkvFilePath, "parent-only-secret=ParentValue\n");
+
+ // Child's .akv file contains the secret the child references.
+ await File.WriteAllTextAsync(childAkvFilePath, "child-connection-secret=Server=tcp:10.0.0.1,1433;Trusted_Connection=True;\n");
+
+ // Parent config with azure-key-vault pointing to the parent's .akv file.
+ string parentConfig = $@"{{
+ ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
+ ""data-source"": {{
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;""
+ }},
+ ""azure-key-vault"": {{
+ ""endpoint"": ""{parentAkvFilePath.Replace("\\", "\\\\")}""
+ }},
+ ""data-source-files"": [""{childFilePath.Replace("\\", "\\\\")}""],
+ ""runtime"": {{
+ ""rest"": {{ ""enabled"": true }},
+ ""graphql"": {{ ""enabled"": true }},
+ ""host"": {{
+ ""cors"": {{ ""origins"": [] }},
+ ""authentication"": {{ ""provider"": ""StaticWebApps"" }}
+ }}
+ }},
+ ""entities"": {{}}
+ }}";
+
+ // Child config with its own azure-key-vault pointing to the child's .akv file,
+ // and a connection string referencing a secret only in the child's vault.
+ string childConfig = $@"{{
+ ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
+ ""data-source"": {{
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""@akv('child-connection-secret')""
+ }},
+ ""azure-key-vault"": {{
+ ""endpoint"": ""{childAkvFilePath.Replace("\\", "\\\\")}""
+ }},
+ ""runtime"": {{
+ ""rest"": {{ ""enabled"": true }},
+ ""graphql"": {{ ""enabled"": true }},
+ ""host"": {{
+ ""cors"": {{ ""origins"": [] }},
+ ""authentication"": {{ ""provider"": ""StaticWebApps"" }}
+ }}
+ }},
+ ""entities"": {{
+ ""ChildOverrideEntity"": {{
+ ""source"": ""dbo.ChildTable"",
+ ""permissions"": [{{ ""role"": ""anonymous"", ""actions"": [""read""] }}]
+ }}
+ }}
+ }}";
+
+ await File.WriteAllTextAsync(childFilePath, childConfig);
+
+ MockFileSystem fs = new(new Dictionary()
+ {
+ { "dab-config.json", new MockFileData(parentConfig) }
+ });
+
+ FileSystemRuntimeConfigLoader loader = new(fs);
+
+ DeserializationVariableReplacementSettings replacementSettings = new(
+ azureKeyVaultOptions: new AzureKeyVaultOptions() { Endpoint = parentAkvFilePath, UserProvidedEndpoint = true },
+ doReplaceEnvVar: true,
+ doReplaceAkvVar: true,
+ envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore);
+
+ Assert.IsTrue(
+ loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig, replacementSettings: replacementSettings),
+ "Config should load successfully when child config has its own AKV options.");
+
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("ChildOverrideEntity"), "Child config entity should be merged into the parent config.");
+
+ // Verify the child's connection string was resolved using the child's AKV file, not the parent's.
+ string childDataSourceName = runtimeConfig.GetDataSourceNameFromEntityName("ChildOverrideEntity");
+ DataSource childDataSource = runtimeConfig.GetDataSourceFromDataSourceName(childDataSourceName);
+ Assert.IsTrue(
+ childDataSource.ConnectionString.Contains("10.0.0.1"),
+ "Child config connection string should be resolved from the child's own AKV file, not the parent's.");
+ }
+ finally
+ {
+ if (File.Exists(parentAkvFilePath))
+ {
+ File.Delete(parentAkvFilePath);
+ }
+
+ if (File.Exists(childAkvFilePath))
+ {
+ File.Delete(childAkvFilePath);
+ }
+
+ if (File.Exists(childFilePath))
+ {
+ File.Delete(childFilePath);
+ }
+ }
+ }
///
/// Validates that when a child config contains @env('...') references to environment variables
/// that do not exist, the config still loads successfully because the child config uses