Skip to content

Commit 4c26d60

Browse files
CopilotRubenCerna2079JerryNixonAniruddh25
authored
Add CLI command dab auto-config (#3086)
## Why make this change? - #2964 Implements CLI command to configure autoentities definitions via command line. Previously, users had to manually edit the config file to add or modify autoentities entries. ## What is this change? Added `auto-config` command that upserts autoentities definitions with support for: - Pattern matching rules (`--patterns.include`, `--patterns.exclude`, `--patterns.name`) - Template configuration for REST, GraphQL, MCP, Health, Cache endpoints - Permissions using standard `role:actions` format **Implementation:** - `AutoConfigOptions.cs` - Command options class following existing CLI patterns - `ConfigGenerator.TryConfigureAutoentities()` - Main handler with builder methods for patterns, template, and permissions - Registered command in `Program.cs` parser **Bug fixes:** - `AutoentityConverter` and `AutoentityTemplateConverter` - Added missing MCP options serialization logic that prevented MCP settings from persisting to config file - `AutoentityTemplateConverter` - Fixed GraphQL template serialization to only write `enabled` property, excluding `type` object (singular/plural) which is determined by generated entities **Improvements:** - Enhanced error messages to display all valid parameter values using `EnumExtensions.GenerateMessageForInvalidInput<T>()` pattern - Cache level errors now show: "Invalid Source Type: {value}. Valid values are: L1,L1L2" - MCP dml-tool errors now show: "Invalid value for template.mcp.dml-tool: {value}. Valid values are: true, false" - **Permissions validation**: Added runtime validation that requires `--permissions` only when creating a new autoentity definition. When updating an existing definition, permissions are optional and preserved from the existing configuration. ## How was this tested? - [x] Unit Tests - [ ] Integration Tests Added 8 unit tests covering: - Create/update operations with patterns and template options - Permission parsing with multiple actions - Error handling for invalid enum values - Multiple coexisting definitions - Permissions validation for new vs. existing definitions ## Sample Request(s) ```bash # Create new definition with patterns and permissions (permissions required for new definitions) dab auto-config my-def \ --patterns.include "dbo.%" "sys.%" \ --patterns.exclude "dbo.internal%" \ --patterns.name "{schema}_{table}" \ --permissions "anonymous:read" # Configure template options (GraphQL now only writes 'enabled', not 'type') dab auto-config my-def \ --template.rest.enabled true \ --template.graphql.enabled true \ --template.cache.enabled true \ --template.cache.ttl-seconds 30 \ --template.cache.level L1L2 # Update existing definition without permissions (permissions optional for updates) dab auto-config my-def \ --template.mcp.dml-tool false \ --template.cache.ttl-seconds 60 ``` **Result in config:** ```json { "autoentities": { "my-def": { "patterns": { "include": ["dbo.%", "sys.%"], "exclude": ["dbo.internal%"], "name": "{schema}_{table}" }, "template": { "rest": { "enabled": true }, "graphql": { "enabled": true }, "mcp": false, "cache": { "enabled": true, "ttl-seconds": 60, "level": "l1l2" } }, "permissions": [ { "role": "anonymous", "actions": [ { "action": "read" } ] } ] } } } ``` **Notes:** - GraphQL template now correctly serializes as `"graphql": { "enabled": true }` without the empty `type` object that was previously being written. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Add CLI command `dab autoentities configure`</issue_title> > <issue_description>New CLI commands need to be added in order to allow the users to change the autoentities properties inside of the config file. > Introduce a new autoentities configure subcommand that operates like an upsert. > These commands need to follow the existing coding pattern that we have for the other CLI commands > > ``` > dab autoe-config {definition-name} --patterns.include value > dab autoe-config {definition-name} --patterns.exclude value > dab autoe-config {definition-name} --patterns.name value > > dab autoe-config {definition-name} --template.mcp.dml-tool value > dab autoe-config {definition-name} --template.rest.enabled value > dab autoe-config {definition-name} --template.graphql.enabled value > dab autoe-config {definition-name} --template.cache.enabled value > dab autoe-config {definition-name} --template.cache.ttl-seconds value > dab autoe-config {definition-name} --template.cache.level value > dab autoe-config {definition-name} --template.health.enabled value > > dab autoe-config {definition-name} --permissions role:actions > ```</issue_description> > --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Co-authored-by: Ruben Cerna <rcernaserna@microsoft.com> Co-authored-by: Jerry Nixon <1749983+JerryNixon@users.noreply.github.com> Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
1 parent e1e4359 commit 4c26d60

File tree

6 files changed

+683
-3
lines changed

6 files changed

+683
-3
lines changed

src/Cli.Tests/AutoConfigTests.cs

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Cli.Tests;
5+
6+
/// <summary>
7+
/// Tests for the auto-config CLI command.
8+
/// </summary>
9+
[TestClass]
10+
public class AutoConfigTests
11+
{
12+
private IFileSystem? _fileSystem;
13+
private FileSystemRuntimeConfigLoader? _runtimeConfigLoader;
14+
15+
[TestInitialize]
16+
public void TestInitialize()
17+
{
18+
_fileSystem = FileSystemUtils.ProvisionMockFileSystem();
19+
_runtimeConfigLoader = new FileSystemRuntimeConfigLoader(_fileSystem);
20+
21+
ILoggerFactory loggerFactory = TestLoggerSupport.ProvisionLoggerFactory();
22+
ConfigGenerator.SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger<ConfigGenerator>());
23+
SetCliUtilsLogger(loggerFactory.CreateLogger<Utils>());
24+
}
25+
26+
[TestCleanup]
27+
public void TestCleanup()
28+
{
29+
_fileSystem = null;
30+
_runtimeConfigLoader = null;
31+
}
32+
33+
/// <summary>
34+
/// Tests that a new autoentities definition is successfully created with patterns.
35+
/// </summary>
36+
[TestMethod]
37+
public void TestCreateAutoentitiesDefinition_WithPatterns()
38+
{
39+
// Arrange
40+
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
41+
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
42+
43+
AutoConfigOptions options = new(
44+
definitionName: "test-def",
45+
patternsInclude: new[] { "dbo.%", "sys.%" },
46+
patternsExclude: new[] { "dbo.internal%" },
47+
patternsName: "{schema}_{table}",
48+
config: TEST_RUNTIME_CONFIG_FILE
49+
);
50+
51+
// Act
52+
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);
53+
54+
// Assert
55+
Assert.IsTrue(success);
56+
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));
57+
Assert.IsNotNull(config.Autoentities);
58+
Assert.IsTrue(config.Autoentities.Autoentities.ContainsKey("test-def"));
59+
60+
Autoentity autoentity = config.Autoentities.Autoentities["test-def"];
61+
Assert.AreEqual(2, autoentity.Patterns.Include.Length);
62+
Assert.AreEqual("dbo.%", autoentity.Patterns.Include[0]);
63+
Assert.AreEqual("sys.%", autoentity.Patterns.Include[1]);
64+
Assert.AreEqual(1, autoentity.Patterns.Exclude.Length);
65+
Assert.AreEqual("dbo.internal%", autoentity.Patterns.Exclude[0]);
66+
Assert.AreEqual("{schema}_{table}", autoentity.Patterns.Name);
67+
}
68+
69+
/// <summary>
70+
/// Tests that template options are correctly configured for an autoentities definition.
71+
/// </summary>
72+
[TestMethod]
73+
public void TestConfigureAutoentitiesDefinition_WithTemplateOptions()
74+
{
75+
// Arrange
76+
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
77+
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
78+
79+
AutoConfigOptions options = new(
80+
definitionName: "test-def",
81+
templateRestEnabled: true,
82+
templateGraphqlEnabled: false,
83+
templateMcpDmlTool: "true",
84+
templateCacheEnabled: true,
85+
templateCacheTtlSeconds: 30,
86+
templateCacheLevel: "L1",
87+
templateHealthEnabled: true,
88+
config: TEST_RUNTIME_CONFIG_FILE
89+
);
90+
91+
// Act
92+
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);
93+
94+
// Assert
95+
Assert.IsTrue(success);
96+
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));
97+
98+
Autoentity autoentity = config.Autoentities!.Autoentities["test-def"];
99+
Assert.IsTrue(autoentity.Template.Rest.Enabled);
100+
Assert.IsFalse(autoentity.Template.GraphQL.Enabled);
101+
Assert.IsTrue(autoentity.Template.Mcp!.DmlToolEnabled);
102+
Assert.AreEqual(true, autoentity.Template.Cache.Enabled);
103+
Assert.AreEqual(30, autoentity.Template.Cache.TtlSeconds);
104+
Assert.AreEqual(EntityCacheLevel.L1, autoentity.Template.Cache.Level);
105+
Assert.IsTrue(autoentity.Template.Health.Enabled);
106+
}
107+
108+
/// <summary>
109+
/// Tests that an existing autoentities definition is successfully updated.
110+
/// </summary>
111+
[TestMethod]
112+
public void TestUpdateExistingAutoentitiesDefinition()
113+
{
114+
// Arrange
115+
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
116+
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
117+
118+
// Create initial definition
119+
AutoConfigOptions initialOptions = new(
120+
definitionName: "test-def",
121+
patternsInclude: new[] { "dbo.%" },
122+
templateCacheTtlSeconds: 10,
123+
permissions: new[] { "anonymous", "read" },
124+
config: TEST_RUNTIME_CONFIG_FILE
125+
);
126+
Assert.IsTrue(ConfigGenerator.TryConfigureAutoentities(initialOptions, _runtimeConfigLoader!, _fileSystem!));
127+
128+
// Update definition
129+
AutoConfigOptions updateOptions = new(
130+
definitionName: "test-def",
131+
patternsExclude: new[] { "dbo.internal%" },
132+
templateCacheTtlSeconds: 60,
133+
permissions: new[] { "authenticated", "create,read,update,delete" },
134+
config: TEST_RUNTIME_CONFIG_FILE
135+
);
136+
137+
// Act
138+
bool success = ConfigGenerator.TryConfigureAutoentities(updateOptions, _runtimeConfigLoader!, _fileSystem!);
139+
140+
// Assert
141+
Assert.IsTrue(success);
142+
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));
143+
144+
Autoentity autoentity = config.Autoentities!.Autoentities["test-def"];
145+
// Include should remain from initial setup
146+
Assert.AreEqual(1, autoentity.Patterns.Include.Length);
147+
Assert.AreEqual("dbo.%", autoentity.Patterns.Include[0]);
148+
// Exclude should be added
149+
Assert.AreEqual(1, autoentity.Patterns.Exclude.Length);
150+
Assert.AreEqual("dbo.internal%", autoentity.Patterns.Exclude[0]);
151+
// Cache TTL should be updated
152+
Assert.AreEqual(60, autoentity.Template.Cache.TtlSeconds);
153+
// Permissions should be replaced
154+
Assert.AreEqual(1, autoentity.Permissions.Length);
155+
Assert.AreEqual("authenticated", autoentity.Permissions[0].Role);
156+
}
157+
158+
/// <summary>
159+
/// Tests that permissions are correctly parsed and applied.
160+
/// </summary>
161+
[TestMethod]
162+
public void TestConfigureAutoentitiesDefinition_WithMultipleActions()
163+
{
164+
// Arrange
165+
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
166+
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
167+
168+
AutoConfigOptions options = new(
169+
definitionName: "test-def",
170+
permissions: new[] { "authenticated", "create,read,update,delete" },
171+
config: TEST_RUNTIME_CONFIG_FILE
172+
);
173+
174+
// Act
175+
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);
176+
177+
// Assert
178+
Assert.IsTrue(success);
179+
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));
180+
181+
Autoentity autoentity = config.Autoentities!.Autoentities["test-def"];
182+
Assert.AreEqual(1, autoentity.Permissions.Length);
183+
Assert.AreEqual("authenticated", autoentity.Permissions[0].Role);
184+
Assert.AreEqual(4, autoentity.Permissions[0].Actions.Length);
185+
}
186+
187+
/// <summary>
188+
/// Tests that invalid MCP dml-tool value is handled correctly.
189+
/// </summary>
190+
[TestMethod]
191+
public void TestConfigureAutoentitiesDefinition_InvalidMcpDmlTool()
192+
{
193+
// Arrange
194+
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
195+
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
196+
197+
AutoConfigOptions options = new(
198+
definitionName: "test-def",
199+
templateMcpDmlTool: "invalid-value",
200+
permissions: new[] { "anonymous", "read" },
201+
config: TEST_RUNTIME_CONFIG_FILE
202+
);
203+
204+
// Act
205+
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);
206+
207+
// Assert - Should fail due to invalid MCP value
208+
Assert.IsFalse(success);
209+
}
210+
211+
/// <summary>
212+
/// Tests that invalid cache level value is handled correctly.
213+
/// </summary>
214+
[TestMethod]
215+
public void TestConfigureAutoentitiesDefinition_InvalidCacheLevel()
216+
{
217+
// Arrange
218+
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
219+
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
220+
221+
AutoConfigOptions options = new(
222+
definitionName: "test-def",
223+
templateCacheLevel: "InvalidLevel",
224+
permissions: new[] { "anonymous", "read" },
225+
config: TEST_RUNTIME_CONFIG_FILE
226+
);
227+
228+
// Act
229+
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);
230+
231+
// Assert - Should fail due to invalid cache level
232+
Assert.IsFalse(success);
233+
}
234+
235+
/// <summary>
236+
/// Tests that multiple autoentities definitions can coexist.
237+
/// </summary>
238+
[TestMethod]
239+
public void TestMultipleAutoentitiesDefinitions()
240+
{
241+
// Arrange
242+
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
243+
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
244+
245+
// Create first definition
246+
AutoConfigOptions options1 = new(
247+
definitionName: "def-1",
248+
patternsInclude: new[] { "dbo.%" },
249+
permissions: new[] { "anonymous", "read" },
250+
config: TEST_RUNTIME_CONFIG_FILE
251+
);
252+
Assert.IsTrue(ConfigGenerator.TryConfigureAutoentities(options1, _runtimeConfigLoader!, _fileSystem!));
253+
254+
// Create second definition
255+
AutoConfigOptions options2 = new(
256+
definitionName: "def-2",
257+
patternsInclude: new[] { "sys.%" },
258+
permissions: new[] { "authenticated", "*" },
259+
config: TEST_RUNTIME_CONFIG_FILE
260+
);
261+
262+
// Act
263+
bool success = ConfigGenerator.TryConfigureAutoentities(options2, _runtimeConfigLoader!, _fileSystem!);
264+
265+
// Assert
266+
Assert.IsTrue(success);
267+
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));
268+
Assert.AreEqual(2, config.Autoentities!.Autoentities.Count);
269+
Assert.IsTrue(config.Autoentities.Autoentities.ContainsKey("def-1"));
270+
Assert.IsTrue(config.Autoentities.Autoentities.ContainsKey("def-2"));
271+
}
272+
273+
/// <summary>
274+
/// Tests that attempting to configure autoentities without a config file fails.
275+
/// </summary>
276+
[TestMethod]
277+
public void TestConfigureAutoentitiesDefinition_NoConfigFile()
278+
{
279+
// Arrange
280+
AutoConfigOptions options = new(
281+
definitionName: "test-def",
282+
permissions: new[] { "anonymous", "read" }
283+
);
284+
285+
// Act
286+
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);
287+
288+
// Assert
289+
Assert.IsFalse(success);
290+
}
291+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.IO.Abstractions;
5+
using Azure.DataApiBuilder.Config;
6+
using Azure.DataApiBuilder.Product;
7+
using Cli.Constants;
8+
using CommandLine;
9+
using Microsoft.Extensions.Logging;
10+
using static Cli.Utils;
11+
using ILogger = Microsoft.Extensions.Logging.ILogger;
12+
13+
namespace Cli.Commands
14+
{
15+
/// <summary>
16+
/// AutoConfigOptions command options
17+
/// This command will be used to configure autoentities definitions in the config file.
18+
/// </summary>
19+
[Verb("auto-config", isDefault: false, HelpText = "Configure autoentities definitions", Hidden = false)]
20+
public class AutoConfigOptions : Options
21+
{
22+
public AutoConfigOptions(
23+
string definitionName,
24+
IEnumerable<string>? patternsInclude = null,
25+
IEnumerable<string>? patternsExclude = null,
26+
string? patternsName = null,
27+
string? templateMcpDmlTool = null,
28+
bool? templateRestEnabled = null,
29+
bool? templateGraphqlEnabled = null,
30+
bool? templateCacheEnabled = null,
31+
int? templateCacheTtlSeconds = null,
32+
string? templateCacheLevel = null,
33+
bool? templateHealthEnabled = null,
34+
IEnumerable<string>? permissions = null,
35+
string? config = null)
36+
: base(config)
37+
{
38+
DefinitionName = definitionName;
39+
PatternsInclude = patternsInclude;
40+
PatternsExclude = patternsExclude;
41+
PatternsName = patternsName;
42+
TemplateMcpDmlTool = templateMcpDmlTool;
43+
TemplateRestEnabled = templateRestEnabled;
44+
TemplateGraphqlEnabled = templateGraphqlEnabled;
45+
TemplateCacheEnabled = templateCacheEnabled;
46+
TemplateCacheTtlSeconds = templateCacheTtlSeconds;
47+
TemplateCacheLevel = templateCacheLevel;
48+
TemplateHealthEnabled = templateHealthEnabled;
49+
Permissions = permissions;
50+
}
51+
52+
[Value(0, Required = true, HelpText = "Name of the autoentities definition to configure.")]
53+
public string DefinitionName { get; }
54+
55+
[Option("patterns.include", Required = false, HelpText = "T-SQL LIKE pattern(s) to include database objects. Space-separated array of patterns. Default: '%.%'.")]
56+
public IEnumerable<string>? PatternsInclude { get; }
57+
58+
[Option("patterns.exclude", Required = false, HelpText = "T-SQL LIKE pattern(s) to exclude database objects. Space-separated array of patterns. Default: null")]
59+
public IEnumerable<string>? PatternsExclude { get; }
60+
61+
[Option("patterns.name", Required = false, HelpText = "Interpolation syntax for entity naming (must be unique for each generated entity). Default: '{object}'")]
62+
public string? PatternsName { get; }
63+
64+
[Option("template.mcp.dml-tool", Required = false, HelpText = "Enable/disable DML tools for generated entities. Allowed values: true, false. Default: true")]
65+
public string? TemplateMcpDmlTool { get; }
66+
67+
[Option("template.rest.enabled", Required = false, HelpText = "Enable/disable REST endpoint for generated entities. Allowed values: true, false. Default: true")]
68+
public bool? TemplateRestEnabled { get; }
69+
70+
[Option("template.graphql.enabled", Required = false, HelpText = "Enable/disable GraphQL endpoint for generated entities. Allowed values: true, false. Default: true")]
71+
public bool? TemplateGraphqlEnabled { get; }
72+
73+
[Option("template.cache.enabled", Required = false, HelpText = "Enable/disable cache for generated entities. Allowed values: true, false. Default: false")]
74+
public bool? TemplateCacheEnabled { get; }
75+
76+
[Option("template.cache.ttl-seconds", Required = false, HelpText = "Cache time-to-live in seconds for generated entities. Default: null")]
77+
public int? TemplateCacheTtlSeconds { get; }
78+
79+
[Option("template.cache.level", Required = false, HelpText = "Cache level for generated entities. Allowed values: L1, L1L2. Default: L1L2")]
80+
public string? TemplateCacheLevel { get; }
81+
82+
[Option("template.health.enabled", Required = false, HelpText = "Enable/disable health check for generated entities. Allowed values: true, false. Default: true")]
83+
public bool? TemplateHealthEnabled { get; }
84+
85+
[Option("permissions", Required = false, Separator = ':', HelpText = "Permissions for generated entities in the format role:actions (e.g., anonymous:read). Default: null")]
86+
public IEnumerable<string>? Permissions { get; }
87+
88+
public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
89+
{
90+
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
91+
bool isSuccess = ConfigGenerator.TryConfigureAutoentities(this, loader, fileSystem);
92+
if (isSuccess)
93+
{
94+
logger.LogInformation("Successfully configured autoentities definition: {DefinitionName}.", DefinitionName);
95+
return CliReturnCode.SUCCESS;
96+
}
97+
else
98+
{
99+
logger.LogError("Failed to configure autoentities definition: {DefinitionName}.", DefinitionName);
100+
return CliReturnCode.GENERAL_ERROR;
101+
}
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)