-
Notifications
You must be signed in to change notification settings - Fork 305
Add CLI command dab autoentities-configure
#3086
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
12
commits into
main
Choose a base branch
from
copilot/add-autoentities-configure-command
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+673
−3
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3d2b37d
Initial plan
Copilot c9d7425
Add autoentities-configure CLI command with all required options
Copilot 3ec334a
Fix MCP options serialization in autoentities template converter
Copilot a3f5ab0
Add comprehensive tests for autoentities-configure command
Copilot f2cdf40
Fix GraphQL type serialization and improve error messages
Copilot 504641d
Add permissions validation for new autoentity definitions
Copilot 7d6684c
Fix permission issues
RubenCerna2079 a047f02
Fix cli tests
RubenCerna2079 015db1d
Merge branch 'main' into copilot/add-autoentities-configure-command
RubenCerna2079 27657f2
Fix unit tests
RubenCerna2079 06ff923
Fix unit tests
RubenCerna2079 4248231
Fix formatting errors
RubenCerna2079 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Cli.Tests; | ||
|
|
||
| /// <summary> | ||
| /// Tests for the autoentities-configure CLI command. | ||
| /// </summary> | ||
| [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<ConfigGenerator>()); | ||
| SetCliUtilsLogger(loggerFactory.CreateLogger<Utils>()); | ||
| } | ||
|
|
||
| [TestCleanup] | ||
| public void TestCleanup() | ||
| { | ||
| _fileSystem = null; | ||
| _runtimeConfigLoader = null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that a new autoentities definition is successfully created with patterns. | ||
| /// </summary> | ||
| [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}", | ||
| 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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that template options are correctly configured for an autoentities definition. | ||
| /// </summary> | ||
| [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, | ||
| 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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that an existing autoentities definition is successfully updated. | ||
| /// </summary> | ||
| [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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that permissions are correctly parsed and applied. | ||
| /// </summary> | ||
| [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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that invalid MCP dml-tool value is handled correctly. | ||
| /// </summary> | ||
| [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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that invalid cache level value is handled correctly. | ||
| /// </summary> | ||
| [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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that multiple autoentities definitions can coexist. | ||
| /// </summary> | ||
| [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")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that attempting to configure autoentities without a config file fails. | ||
| /// </summary> | ||
| [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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| { | ||
| /// <summary> | ||
| /// AutoentitiesConfigureOptions command options | ||
| /// This command will be used to configure autoentities definitions in the config file. | ||
| /// </summary> | ||
| [Verb("autoentities-configure", isDefault: false, HelpText = "Configure autoentities definitions", Hidden = false)] | ||
RubenCerna2079 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public class AutoentitiesConfigureOptions : Options | ||
| { | ||
| public AutoentitiesConfigureOptions( | ||
| string definitionName, | ||
| IEnumerable<string>? patternsInclude = null, | ||
| IEnumerable<string>? 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<string>? 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<string>? PatternsInclude { get; } | ||
|
|
||
| [Option("patterns.exclude", Required = false, HelpText = "T-SQL LIKE pattern(s) to exclude database objects. Space-separated array of patterns.")] | ||
| public IEnumerable<string>? 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; } | ||
RubenCerna2079 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| [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).")] | ||
RubenCerna2079 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public IEnumerable<string>? 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; | ||
| } | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unit tests validate
Template.Mcp.DmlToolEnabledon the in-memory object, but they don’t assert the actual JSON persisted to disk. GivenEntityMcpOptionscan serialize as a boolean shorthand, it would be good to add an assertion that the written config containsautoentities.<def>.template.mcpas an object with adml-toolsproperty (matching the schema), and that GraphQL template serialization only writesenabled(notype). This would catch regressions in the converters.