From 3d2b37d8757bda961725eac15033fa296f8d9b31 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 30 Jan 2026 20:07:36 +0000
Subject: [PATCH 1/6] Initial plan
From c9d74259fc90409da7935c43fe5e1ba3a2a9e798 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 30 Jan 2026 20:14:06 +0000
Subject: [PATCH 2/6] Add autoentities-configure CLI command with all required
options
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
---
.../Commands/AutoentitiesConfigureOptions.cs | 104 +++++++
src/Cli/ConfigGenerator.cs | 255 ++++++++++++++++++
src/Cli/Program.cs | 3 +-
3 files changed, 361 insertions(+), 1 deletion(-)
create mode 100644 src/Cli/Commands/AutoentitiesConfigureOptions.cs
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..606152591e 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2747,6 +2747,261 @@ 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);
+
+ // 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.
+ ///
+ 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 existingAutoentity?.Template ?? new AutoentityTemplate();
+ }
+
+ bool? customToolEnabled = mcp?.CustomToolEnabled;
+ mcp = new EntityMcpOptions(customToolEnabled: customToolEnabled, dmlToolsEnabled: mcpDmlToolValue);
+ 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 existingAutoentity?.Template ?? new AutoentityTemplate();
+ }
+
+ 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));
From 3ec334a9b8866f2de31015ac32a46c9801e49833 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 30 Jan 2026 20:18:32 +0000
Subject: [PATCH 3/6] Fix MCP options serialization in autoentities template
converter
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
---
src/Cli/ConfigGenerator.cs | 5 +++--
src/Config/Converters/AutoentityConverter.cs | 1 +
src/Config/Converters/AutoentityTemplateConverter.cs | 6 ++++++
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 606152591e..dae64006ed 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2893,8 +2893,9 @@ private static AutoentityTemplate BuildAutoentityTemplate(AutoentitiesConfigureO
return existingAutoentity?.Template ?? new AutoentityTemplate();
}
- bool? customToolEnabled = mcp?.CustomToolEnabled;
- mcp = new EntityMcpOptions(customToolEnabled: customToolEnabled, dmlToolsEnabled: mcpDmlToolValue);
+ 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);
}
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");
From a3f5ab058ade84be1236d15459d56db105ce5975 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 30 Jan 2026 20:26:41 +0000
Subject: [PATCH 4/6] Add comprehensive tests for autoentities-configure
command
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
---
src/Cli.Tests/AutoentitiesConfigureTests.cs | 305 ++++++++++++++++++++
src/Cli/ConfigGenerator.cs | 13 +-
2 files changed, 314 insertions(+), 4 deletions(-)
create mode 100644 src/Cli.Tests/AutoentitiesConfigureTests.cs
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/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index dae64006ed..1ae9303ab4 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2784,7 +2784,11 @@ public static bool TryConfigureAutoentities(AutoentitiesConfigureOptions options
AutoentityPatterns patterns = BuildAutoentityPatterns(options, existingAutoentity);
// Build template
- AutoentityTemplate template = BuildAutoentityTemplate(options, existingAutoentity);
+ AutoentityTemplate? template = BuildAutoentityTemplate(options, existingAutoentity);
+ if (template is null)
+ {
+ return false;
+ }
// Build permissions
EntityPermission[]? permissions = BuildAutoentityPermissions(options, existingAutoentity);
@@ -2868,8 +2872,9 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoentitiesConfigureO
///
/// Builds the AutoentityTemplate object from the provided options and existing autoentity.
+ /// Returns null if validation fails.
///
- private static AutoentityTemplate BuildAutoentityTemplate(AutoentitiesConfigureOptions options, Autoentity? existingAutoentity)
+ private static AutoentityTemplate? BuildAutoentityTemplate(AutoentitiesConfigureOptions options, Autoentity? existingAutoentity)
{
// Start with existing values or defaults
EntityMcpOptions? mcp = existingAutoentity?.Template.Mcp;
@@ -2890,7 +2895,7 @@ private static AutoentityTemplate BuildAutoentityTemplate(AutoentitiesConfigureO
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 existingAutoentity?.Template ?? new AutoentityTemplate();
+ return null;
}
bool? customToolEnabled = mcp?.UserProvidedCustomToolEnabled == true ? mcp.CustomToolEnabled : null;
@@ -2953,7 +2958,7 @@ private static AutoentityTemplate BuildAutoentityTemplate(AutoentitiesConfigureO
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 existingAutoentity?.Template ?? new AutoentityTemplate();
+ return null;
}
cacheLevel = cacheLevelValue;
From f2cdf40b07b7962d4ef2f587d49c5b39190f3318 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 5 Feb 2026 23:11:13 +0000
Subject: [PATCH 5/6] Fix GraphQL type serialization and improve error messages
- Fix GraphQL template to only serialize 'enabled' property (not 'type')
- Improve error messages to show all valid parameter values
- Use EnumExtensions.GenerateMessageForInvalidInput for EntityCacheLevel
- Update MCP dml-tool error message to show valid values
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
---
src/Cli/ConfigGenerator.cs | 4 ++--
src/Config/Converters/AutoentityTemplateConverter.cs | 6 +++++-
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 1ae9303ab4..5c4cce86e2 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2894,7 +2894,7 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoentitiesConfigureO
{
if (!bool.TryParse(options.TemplateMcpDmlTool, out bool mcpDmlToolValue))
{
- _logger.LogError("Invalid value for template.mcp.dml-tool: {value}. Expected: true or false.", options.TemplateMcpDmlTool);
+ _logger.LogError("Invalid value for template.mcp.dml-tool: {value}. Valid values are: true, false", options.TemplateMcpDmlTool);
return null;
}
@@ -2957,7 +2957,7 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoentitiesConfigureO
{
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);
+ _logger.LogError(EnumExtensions.GenerateMessageForInvalidInput(options.TemplateCacheLevel));
return null;
}
diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs
index 2f5ea3407f..8f5cb1276c 100644
--- a/src/Config/Converters/AutoentityTemplateConverter.cs
+++ b/src/Config/Converters/AutoentityTemplateConverter.cs
@@ -113,7 +113,11 @@ public override void Write(Utf8JsonWriter writer, AutoentityTemplate value, Json
if (value?.UserProvidedGraphQLOptions is true)
{
writer.WritePropertyName("graphql");
- JsonSerializer.Serialize(writer, value.GraphQL, options);
+ // For autoentities template, only write the enabled property
+ // The type (singular/plural) is determined by the generated entities
+ writer.WriteStartObject();
+ writer.WriteBoolean("enabled", value.GraphQL.Enabled);
+ writer.WriteEndObject();
}
if (value?.UserProvidedMcpOptions is true)
From 504641d485ef2d3bd6f976535997e02a2d8a1652 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 00:14:04 +0000
Subject: [PATCH 6/6] Add permissions validation for new autoentity definitions
- Require permissions when creating a new autoentity definition
- Allow updating existing definitions without providing permissions
- Improve error handling to distinguish between parsing failure and missing permissions
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
---
src/Cli/ConfigGenerator.cs | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 5c4cce86e2..d523ea1356 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2792,12 +2792,22 @@ public static bool TryConfigureAutoentities(AutoentitiesConfigureOptions options
// Build permissions
EntityPermission[]? permissions = BuildAutoentityPermissions(options, existingAutoentity);
- if (permissions is null && options.Permissions is not null)
+
+ // Check if permissions parsing failed (non-empty input but failed to parse)
+ bool permissionsProvided = options.Permissions is not null && options.Permissions.Any();
+ if (permissions is null && permissionsProvided)
{
_logger.LogError("Failed to parse permissions.");
return false;
}
+ // Permissions are required when creating a new autoentity definition
+ if (existingAutoentity is null && (permissions is null || permissions.Length == 0))
+ {
+ _logger.LogError("Permissions are required when creating a new autoentities definition. Use --permissions \"role:actions\"");
+ return false;
+ }
+
// Create updated autoentity
Autoentity updatedAutoentity = new(
Patterns: patterns,