diff --git a/src/Cli.Tests/AutoentitiesConfigureTests.cs b/src/Cli.Tests/AutoentitiesConfigureTests.cs new file mode 100644 index 0000000000..a0d1ae9208 --- /dev/null +++ b/src/Cli.Tests/AutoentitiesConfigureTests.cs @@ -0,0 +1,305 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Abstractions; +using System.Text; +using System.Text.Json; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.ObjectModel; +using Cli.Commands; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using static Cli.Utils; +using static Cli.Tests.TestHelper; + +namespace Cli.Tests; + +/// +/// Tests for the autoentities-configure CLI command. +/// +[TestClass] +public class AutoentitiesConfigureTests +{ + private IFileSystem? _fileSystem; + private FileSystemRuntimeConfigLoader? _runtimeConfigLoader; + + [TestInitialize] + public void TestInitialize() + { + _fileSystem = FileSystemUtils.ProvisionMockFileSystem(); + _runtimeConfigLoader = new FileSystemRuntimeConfigLoader(_fileSystem); + + ILoggerFactory loggerFactory = TestLoggerSupport.ProvisionLoggerFactory(); + ConfigGenerator.SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger()); + SetCliUtilsLogger(loggerFactory.CreateLogger()); + } + + [TestCleanup] + public void TestCleanup() + { + _fileSystem = null; + _runtimeConfigLoader = null; + } + + /// + /// Tests that a new autoentities definition is successfully created with patterns. + /// + [TestMethod] + public void TestCreateAutoentitiesDefinition_WithPatterns() + { + // Arrange + InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!)); + + AutoentitiesConfigureOptions options = new( + definitionName: "test-def", + patternsInclude: new[] { "dbo.%", "sys.%" }, + patternsExclude: new[] { "dbo.internal%" }, + patternsName: "{schema}_{table}", + permissions: new[] { "anonymous", "read" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(success); + Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config)); + Assert.IsNotNull(config.Autoentities); + Assert.IsTrue(config.Autoentities.AutoEntities.ContainsKey("test-def")); + + Autoentity autoentity = config.Autoentities.AutoEntities["test-def"]; + Assert.AreEqual(2, autoentity.Patterns.Include.Length); + Assert.AreEqual("dbo.%", autoentity.Patterns.Include[0]); + Assert.AreEqual("sys.%", autoentity.Patterns.Include[1]); + Assert.AreEqual(1, autoentity.Patterns.Exclude.Length); + Assert.AreEqual("dbo.internal%", autoentity.Patterns.Exclude[0]); + Assert.AreEqual("{schema}_{table}", autoentity.Patterns.Name); + } + + /// + /// Tests that template options are correctly configured for an autoentities definition. + /// + [TestMethod] + public void TestConfigureAutoentitiesDefinition_WithTemplateOptions() + { + // Arrange + InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!)); + + AutoentitiesConfigureOptions options = new( + definitionName: "test-def", + templateRestEnabled: true, + templateGraphqlEnabled: false, + templateMcpDmlTool: "true", + templateCacheEnabled: true, + templateCacheTtlSeconds: 30, + templateCacheLevel: "L1", + templateHealthEnabled: true, + permissions: new[] { "anonymous", "read" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(success); + Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config)); + + Autoentity autoentity = config.Autoentities!.AutoEntities["test-def"]; + Assert.IsTrue(autoentity.Template.Rest.Enabled); + Assert.IsFalse(autoentity.Template.GraphQL.Enabled); + Assert.IsTrue(autoentity.Template.Mcp!.DmlToolEnabled); + Assert.AreEqual(true, autoentity.Template.Cache.Enabled); + Assert.AreEqual(30, autoentity.Template.Cache.TtlSeconds); + Assert.AreEqual(EntityCacheLevel.L1, autoentity.Template.Cache.Level); + Assert.IsTrue(autoentity.Template.Health.Enabled); + } + + /// + /// Tests that an existing autoentities definition is successfully updated. + /// + [TestMethod] + public void TestUpdateExistingAutoentitiesDefinition() + { + // Arrange + InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!)); + + // Create initial definition + AutoentitiesConfigureOptions initialOptions = new( + definitionName: "test-def", + patternsInclude: new[] { "dbo.%" }, + templateCacheTtlSeconds: 10, + permissions: new[] { "anonymous", "read" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + Assert.IsTrue(ConfigGenerator.TryConfigureAutoentities(initialOptions, _runtimeConfigLoader!, _fileSystem!)); + + // Update definition + AutoentitiesConfigureOptions updateOptions = new( + definitionName: "test-def", + patternsExclude: new[] { "dbo.internal%" }, + templateCacheTtlSeconds: 60, + permissions: new[] { "authenticated", "create,read,update,delete" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(updateOptions, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(success); + Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config)); + + Autoentity autoentity = config.Autoentities!.AutoEntities["test-def"]; + // Include should remain from initial setup + Assert.AreEqual(1, autoentity.Patterns.Include.Length); + Assert.AreEqual("dbo.%", autoentity.Patterns.Include[0]); + // Exclude should be added + Assert.AreEqual(1, autoentity.Patterns.Exclude.Length); + Assert.AreEqual("dbo.internal%", autoentity.Patterns.Exclude[0]); + // Cache TTL should be updated + Assert.AreEqual(60, autoentity.Template.Cache.TtlSeconds); + // Permissions should be replaced + Assert.AreEqual(1, autoentity.Permissions.Length); + Assert.AreEqual("authenticated", autoentity.Permissions[0].Role); + } + + /// + /// Tests that permissions are correctly parsed and applied. + /// + [TestMethod] + public void TestConfigureAutoentitiesDefinition_WithMultipleActions() + { + // Arrange + InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!)); + + AutoentitiesConfigureOptions options = new( + definitionName: "test-def", + permissions: new[] { "authenticated", "create,read,update,delete" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(success); + Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config)); + + Autoentity autoentity = config.Autoentities!.AutoEntities["test-def"]; + Assert.AreEqual(1, autoentity.Permissions.Length); + Assert.AreEqual("authenticated", autoentity.Permissions[0].Role); + Assert.AreEqual(4, autoentity.Permissions[0].Actions.Length); + } + + /// + /// Tests that invalid MCP dml-tool value is handled correctly. + /// + [TestMethod] + public void TestConfigureAutoentitiesDefinition_InvalidMcpDmlTool() + { + // Arrange + InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!)); + + AutoentitiesConfigureOptions options = new( + definitionName: "test-def", + templateMcpDmlTool: "invalid-value", + permissions: new[] { "anonymous", "read" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert - Should fail due to invalid MCP value + Assert.IsFalse(success); + } + + /// + /// Tests that invalid cache level value is handled correctly. + /// + [TestMethod] + public void TestConfigureAutoentitiesDefinition_InvalidCacheLevel() + { + // Arrange + InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!)); + + AutoentitiesConfigureOptions options = new( + definitionName: "test-def", + templateCacheLevel: "InvalidLevel", + permissions: new[] { "anonymous", "read" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert - Should fail due to invalid cache level + Assert.IsFalse(success); + } + + /// + /// Tests that multiple autoentities definitions can coexist. + /// + [TestMethod] + public void TestMultipleAutoentitiesDefinitions() + { + // Arrange + InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!)); + + // Create first definition + AutoentitiesConfigureOptions options1 = new( + definitionName: "def-1", + patternsInclude: new[] { "dbo.%" }, + permissions: new[] { "anonymous", "read" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + Assert.IsTrue(ConfigGenerator.TryConfigureAutoentities(options1, _runtimeConfigLoader!, _fileSystem!)); + + // Create second definition + AutoentitiesConfigureOptions options2 = new( + definitionName: "def-2", + patternsInclude: new[] { "sys.%" }, + permissions: new[] { "authenticated", "*" }, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(options2, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(success); + Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config)); + Assert.AreEqual(2, config.Autoentities!.AutoEntities.Count); + Assert.IsTrue(config.Autoentities.AutoEntities.ContainsKey("def-1")); + Assert.IsTrue(config.Autoentities.AutoEntities.ContainsKey("def-2")); + } + + /// + /// Tests that attempting to configure autoentities without a config file fails. + /// + [TestMethod] + public void TestConfigureAutoentitiesDefinition_NoConfigFile() + { + // Arrange + AutoentitiesConfigureOptions options = new( + definitionName: "test-def", + permissions: new[] { "anonymous", "read" } + ); + + // Act + bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsFalse(success); + } +} diff --git a/src/Cli/Commands/AutoentitiesConfigureOptions.cs b/src/Cli/Commands/AutoentitiesConfigureOptions.cs new file mode 100644 index 0000000000..bf6b7357e9 --- /dev/null +++ b/src/Cli/Commands/AutoentitiesConfigureOptions.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Abstractions; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Product; +using Cli.Constants; +using CommandLine; +using Microsoft.Extensions.Logging; +using static Cli.Utils; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Cli.Commands +{ + /// + /// AutoentitiesConfigureOptions command options + /// This command will be used to configure autoentities definitions in the config file. + /// + [Verb("autoentities-configure", isDefault: false, HelpText = "Configure autoentities definitions", Hidden = false)] + public class AutoentitiesConfigureOptions : Options + { + public AutoentitiesConfigureOptions( + string definitionName, + IEnumerable? patternsInclude = null, + IEnumerable? patternsExclude = null, + string? patternsName = null, + string? templateMcpDmlTool = null, + bool? templateRestEnabled = null, + bool? templateGraphqlEnabled = null, + bool? templateCacheEnabled = null, + int? templateCacheTtlSeconds = null, + string? templateCacheLevel = null, + bool? templateHealthEnabled = null, + IEnumerable? permissions = null, + string? config = null) + : base(config) + { + DefinitionName = definitionName; + PatternsInclude = patternsInclude; + PatternsExclude = patternsExclude; + PatternsName = patternsName; + TemplateMcpDmlTool = templateMcpDmlTool; + TemplateRestEnabled = templateRestEnabled; + TemplateGraphqlEnabled = templateGraphqlEnabled; + TemplateCacheEnabled = templateCacheEnabled; + TemplateCacheTtlSeconds = templateCacheTtlSeconds; + TemplateCacheLevel = templateCacheLevel; + TemplateHealthEnabled = templateHealthEnabled; + Permissions = permissions; + } + + [Value(0, Required = true, HelpText = "Name of the autoentities definition to configure.")] + public string DefinitionName { get; } + + [Option("patterns.include", Required = false, HelpText = "T-SQL LIKE pattern(s) to include database objects. Space-separated array of patterns.")] + public IEnumerable? PatternsInclude { get; } + + [Option("patterns.exclude", Required = false, HelpText = "T-SQL LIKE pattern(s) to exclude database objects. Space-separated array of patterns.")] + public IEnumerable? PatternsExclude { get; } + + [Option("patterns.name", Required = false, HelpText = "Interpolation syntax for entity naming (must be unique for each generated entity).")] + public string? PatternsName { get; } + + [Option("template.mcp.dml-tool", Required = false, HelpText = "Enable/disable DML tools for generated entities. Allowed values: true, false.")] + public string? TemplateMcpDmlTool { get; } + + [Option("template.rest.enabled", Required = false, HelpText = "Enable/disable REST endpoint for generated entities. Allowed values: true, false.")] + public bool? TemplateRestEnabled { get; } + + [Option("template.graphql.enabled", Required = false, HelpText = "Enable/disable GraphQL endpoint for generated entities. Allowed values: true, false.")] + public bool? TemplateGraphqlEnabled { get; } + + [Option("template.cache.enabled", Required = false, HelpText = "Enable/disable cache for generated entities. Allowed values: true, false.")] + public bool? TemplateCacheEnabled { get; } + + [Option("template.cache.ttl-seconds", Required = false, HelpText = "Cache time-to-live in seconds for generated entities.")] + public int? TemplateCacheTtlSeconds { get; } + + [Option("template.cache.level", Required = false, HelpText = "Cache level for generated entities. Allowed values: L1, L1L2.")] + public string? TemplateCacheLevel { get; } + + [Option("template.health.enabled", Required = false, HelpText = "Enable/disable health check for generated entities. Allowed values: true, false.")] + public bool? TemplateHealthEnabled { get; } + + [Option("permissions", Required = false, Separator = ':', HelpText = "Permissions for generated entities in the format role:actions (e.g., anonymous:read).")] + public IEnumerable? Permissions { get; } + + public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) + { + logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion()); + bool isSuccess = ConfigGenerator.TryConfigureAutoentities(this, loader, fileSystem); + if (isSuccess) + { + logger.LogInformation("Successfully configured autoentities definition: {DefinitionName}.", DefinitionName); + return CliReturnCode.SUCCESS; + } + else + { + logger.LogError("Failed to configure autoentities definition: {DefinitionName}.", DefinitionName); + return CliReturnCode.GENERAL_ERROR; + } + } + } +} diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 78a5e63a7d..1ae9303ab4 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -2747,6 +2747,267 @@ public static bool TryAddTelemetry(AddTelemetryOptions options, FileSystemRuntim return WriteRuntimeConfigToFile(runtimeConfigFile, runtimeConfig, fileSystem); } + /// + /// Configures an autoentities definition in the runtime config. + /// This method updates or creates an autoentities definition with the specified patterns, template, and permissions. + /// + /// The autoentities configuration options provided by the user. + /// The config loader to read the existing config. + /// The filesystem used for reading and writing the config file. + /// True if the autoentities definition was successfully configured; otherwise, false. + public static bool TryConfigureAutoentities(AutoentitiesConfigureOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) + { + if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile)) + { + return false; + } + + if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig)) + { + _logger.LogError("Failed to read the config file: {runtimeConfigFile}.", runtimeConfigFile); + return false; + } + + // Get existing autoentities or create new collection + Dictionary autoEntitiesDictionary = runtimeConfig.Autoentities?.AutoEntities != null + ? new Dictionary(runtimeConfig.Autoentities.AutoEntities) + : new Dictionary(); + + // Get existing autoentity definition or create a new one + Autoentity? existingAutoentity = null; + if (autoEntitiesDictionary.TryGetValue(options.DefinitionName, out Autoentity? value)) + { + existingAutoentity = value; + } + + // Build patterns + AutoentityPatterns patterns = BuildAutoentityPatterns(options, existingAutoentity); + + // Build template + AutoentityTemplate? template = BuildAutoentityTemplate(options, existingAutoentity); + if (template is null) + { + return false; + } + + // Build permissions + EntityPermission[]? permissions = BuildAutoentityPermissions(options, existingAutoentity); + if (permissions is null && options.Permissions is not null) + { + _logger.LogError("Failed to parse permissions."); + return false; + } + + // Create updated autoentity + Autoentity updatedAutoentity = new( + Patterns: patterns, + Template: template, + Permissions: permissions ?? (existingAutoentity?.Permissions ?? Array.Empty()) + ); + + // Update the dictionary + autoEntitiesDictionary[options.DefinitionName] = updatedAutoentity; + + // Update runtime config + runtimeConfig = runtimeConfig with + { + Autoentities = new RuntimeAutoentities(autoEntitiesDictionary) + }; + + return WriteRuntimeConfigToFile(runtimeConfigFile, runtimeConfig, fileSystem); + } + + /// + /// Builds the AutoentityPatterns object from the provided options and existing autoentity. + /// + private static AutoentityPatterns BuildAutoentityPatterns(AutoentitiesConfigureOptions options, Autoentity? existingAutoentity) + { + string[]? include = null; + string[]? exclude = null; + string? name = null; + bool userProvidedInclude = false; + bool userProvidedExclude = false; + bool userProvidedName = false; + + // Start with existing values + if (existingAutoentity is not null) + { + include = existingAutoentity.Patterns.Include; + exclude = existingAutoentity.Patterns.Exclude; + name = existingAutoentity.Patterns.Name; + userProvidedInclude = existingAutoentity.Patterns.UserProvidedIncludeOptions; + userProvidedExclude = existingAutoentity.Patterns.UserProvidedExcludeOptions; + userProvidedName = existingAutoentity.Patterns.UserProvidedNameOptions; + } + + // Override with new values if provided + if (options.PatternsInclude is not null && options.PatternsInclude.Any()) + { + include = options.PatternsInclude.ToArray(); + userProvidedInclude = true; + _logger.LogInformation("Updated patterns.include for definition '{DefinitionName}'", options.DefinitionName); + } + + if (options.PatternsExclude is not null && options.PatternsExclude.Any()) + { + exclude = options.PatternsExclude.ToArray(); + userProvidedExclude = true; + _logger.LogInformation("Updated patterns.exclude for definition '{DefinitionName}'", options.DefinitionName); + } + + if (!string.IsNullOrWhiteSpace(options.PatternsName)) + { + name = options.PatternsName; + userProvidedName = true; + _logger.LogInformation("Updated patterns.name for definition '{DefinitionName}'", options.DefinitionName); + } + + return new AutoentityPatterns(Include: include, Exclude: exclude, Name: name) + { + UserProvidedIncludeOptions = userProvidedInclude, + UserProvidedExcludeOptions = userProvidedExclude, + UserProvidedNameOptions = userProvidedName + }; + } + + /// + /// Builds the AutoentityTemplate object from the provided options and existing autoentity. + /// Returns null if validation fails. + /// + private static AutoentityTemplate? BuildAutoentityTemplate(AutoentitiesConfigureOptions options, Autoentity? existingAutoentity) + { + // Start with existing values or defaults + EntityMcpOptions? mcp = existingAutoentity?.Template.Mcp; + EntityRestOptions rest = existingAutoentity?.Template.Rest ?? new EntityRestOptions(); + EntityGraphQLOptions graphQL = existingAutoentity?.Template.GraphQL ?? new EntityGraphQLOptions(string.Empty, string.Empty); + EntityHealthCheckConfig health = existingAutoentity?.Template.Health ?? new EntityHealthCheckConfig(); + EntityCacheOptions cache = existingAutoentity?.Template.Cache ?? new EntityCacheOptions(); + + bool userProvidedMcp = existingAutoentity?.Template.UserProvidedMcpOptions ?? false; + bool userProvidedRest = existingAutoentity?.Template.UserProvidedRestOptions ?? false; + bool userProvidedGraphQL = existingAutoentity?.Template.UserProvidedGraphQLOptions ?? false; + bool userProvidedHealth = existingAutoentity?.Template.UserProvidedHealthOptions ?? false; + bool userProvidedCache = existingAutoentity?.Template.UserProvidedCacheOptions ?? false; + + // Update MCP options + if (!string.IsNullOrWhiteSpace(options.TemplateMcpDmlTool)) + { + if (!bool.TryParse(options.TemplateMcpDmlTool, out bool mcpDmlToolValue)) + { + _logger.LogError("Invalid value for template.mcp.dml-tool: {value}. Expected: true or false.", options.TemplateMcpDmlTool); + return null; + } + + bool? customToolEnabled = mcp?.UserProvidedCustomToolEnabled == true ? mcp.CustomToolEnabled : null; + bool? dmlToolValue = mcpDmlToolValue; + mcp = new EntityMcpOptions(customToolEnabled: customToolEnabled, dmlToolsEnabled: dmlToolValue); + userProvidedMcp = true; + _logger.LogInformation("Updated template.mcp.dml-tool for definition '{DefinitionName}'", options.DefinitionName); + } + + // Update REST options + if (options.TemplateRestEnabled is not null) + { + rest = rest with { Enabled = options.TemplateRestEnabled.Value }; + userProvidedRest = true; + _logger.LogInformation("Updated template.rest.enabled for definition '{DefinitionName}'", options.DefinitionName); + } + + // Update GraphQL options + if (options.TemplateGraphqlEnabled is not null) + { + graphQL = graphQL with { Enabled = options.TemplateGraphqlEnabled.Value }; + userProvidedGraphQL = true; + _logger.LogInformation("Updated template.graphql.enabled for definition '{DefinitionName}'", options.DefinitionName); + } + + // Update Health options + if (options.TemplateHealthEnabled is not null) + { + health = new EntityHealthCheckConfig( + enabled: options.TemplateHealthEnabled.Value, + first: health.UserProvidedFirst ? health.First : null, + thresholdMs: health.UserProvidedThresholdMs ? health.ThresholdMs : null + ); + userProvidedHealth = true; + _logger.LogInformation("Updated template.health.enabled for definition '{DefinitionName}'", options.DefinitionName); + } + + // Update Cache options + bool cacheUpdated = false; + bool? cacheEnabled = cache.Enabled; + int? cacheTtl = cache.UserProvidedTtlOptions ? cache.TtlSeconds : null; + EntityCacheLevel? cacheLevel = cache.UserProvidedLevelOptions ? cache.Level : null; + + if (options.TemplateCacheEnabled is not null) + { + cacheEnabled = options.TemplateCacheEnabled.Value; + cacheUpdated = true; + _logger.LogInformation("Updated template.cache.enabled for definition '{DefinitionName}'", options.DefinitionName); + } + + if (options.TemplateCacheTtlSeconds is not null) + { + cacheTtl = options.TemplateCacheTtlSeconds.Value; + cacheUpdated = true; + _logger.LogInformation("Updated template.cache.ttl-seconds for definition '{DefinitionName}'", options.DefinitionName); + } + + if (!string.IsNullOrWhiteSpace(options.TemplateCacheLevel)) + { + if (!Enum.TryParse(options.TemplateCacheLevel, ignoreCase: true, out EntityCacheLevel cacheLevelValue)) + { + _logger.LogError("Invalid value for template.cache.level: {value}. Allowed values: L1, L1L2.", options.TemplateCacheLevel); + return null; + } + + cacheLevel = cacheLevelValue; + cacheUpdated = true; + _logger.LogInformation("Updated template.cache.level for definition '{DefinitionName}'", options.DefinitionName); + } + + if (cacheUpdated) + { + cache = new EntityCacheOptions(Enabled: cacheEnabled, TtlSeconds: cacheTtl, Level: cacheLevel); + userProvidedCache = true; + } + + return new AutoentityTemplate( + Rest: rest, + GraphQL: graphQL, + Mcp: mcp, + Health: health, + Cache: cache + ) + { + UserProvidedMcpOptions = userProvidedMcp, + UserProvidedRestOptions = userProvidedRest, + UserProvidedGraphQLOptions = userProvidedGraphQL, + UserProvidedHealthOptions = userProvidedHealth, + UserProvidedCacheOptions = userProvidedCache + }; + } + + /// + /// Builds the permissions array from the provided options and existing autoentity. + /// + private static EntityPermission[]? BuildAutoentityPermissions(AutoentitiesConfigureOptions options, Autoentity? existingAutoentity) + { + if (options.Permissions is null || !options.Permissions.Any()) + { + return existingAutoentity?.Permissions; + } + + // Parse the permissions + EntityPermission[]? parsedPermissions = ParsePermission(options.Permissions, null, null, null); + if (parsedPermissions is not null) + { + _logger.LogInformation("Updated permissions for definition '{DefinitionName}'", options.DefinitionName); + } + + return parsedPermissions; + } + /// /// Attempts to update the Azure Key Vault configuration options based on the provided values. /// Validates that any user-provided parameter value is valid and updates the runtime configuration accordingly. diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs index 036f3dc2a3..c2662f01e9 100644 --- a/src/Cli/Program.cs +++ b/src/Cli/Program.cs @@ -58,7 +58,7 @@ public static int Execute(string[] args, ILogger cliLogger, IFileSystem fileSyst }); // Parsing user arguments and executing required methods. - int result = parser.ParseArguments(args) + int result = parser.ParseArguments(args) .MapResult( (InitOptions options) => options.Handler(cliLogger, loader, fileSystem), (AddOptions options) => options.Handler(cliLogger, loader, fileSystem), @@ -67,6 +67,7 @@ public static int Execute(string[] args, ILogger cliLogger, IFileSystem fileSyst (ValidateOptions options) => options.Handler(cliLogger, loader, fileSystem), (AddTelemetryOptions options) => options.Handler(cliLogger, loader, fileSystem), (ConfigureOptions options) => options.Handler(cliLogger, loader, fileSystem), + (AutoentitiesConfigureOptions options) => options.Handler(cliLogger, loader, fileSystem), (ExportOptions options) => options.Handler(cliLogger, loader, fileSystem), errors => DabCliParserErrorHandler.ProcessErrorsAndReturnExitCode(errors)); diff --git a/src/Config/Converters/AutoentityConverter.cs b/src/Config/Converters/AutoentityConverter.cs index 5c09ed8e7b..47b6414ae4 100644 --- a/src/Config/Converters/AutoentityConverter.cs +++ b/src/Config/Converters/AutoentityConverter.cs @@ -90,6 +90,7 @@ public override void Write(Utf8JsonWriter writer, Autoentity value, JsonSerializ AutoentityTemplate? template = value?.Template; if (template?.UserProvidedRestOptions is true || template?.UserProvidedGraphQLOptions is true + || template?.UserProvidedMcpOptions is true || template?.UserProvidedHealthOptions is true || template?.UserProvidedCacheOptions is true) { diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs index 275cfc4314..2f5ea3407f 100644 --- a/src/Config/Converters/AutoentityTemplateConverter.cs +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -116,6 +116,12 @@ public override void Write(Utf8JsonWriter writer, AutoentityTemplate value, Json JsonSerializer.Serialize(writer, value.GraphQL, options); } + if (value?.UserProvidedMcpOptions is true) + { + writer.WritePropertyName("mcp"); + JsonSerializer.Serialize(writer, value.Mcp, options); + } + if (value?.UserProvidedHealthOptions is true) { writer.WritePropertyName("health");