From 81330ac26e95ca6250d77ae28785c63cf6e463c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:43:27 +0000 Subject: [PATCH 01/25] Initial plan From 7cfbd5dbdd1742e74322763a0b2f1b26b5d84c6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:00:06 +0000 Subject: [PATCH 02/25] Add autoentities schema and C# models for wildcard properties Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> --- schemas/dab.draft.schema.json | 137 ++++++++++++++++++ .../RuntimeAutoEntitiesConverter.cs | 36 +++++ src/Config/ObjectModel/AutoEntity.cs | 18 +++ src/Config/ObjectModel/AutoEntityPatterns.cs | 18 +++ src/Config/ObjectModel/AutoEntityTemplate.cs | 54 +++++++ src/Config/ObjectModel/RuntimeAutoEntities.cs | 81 +++++++++++ src/Config/ObjectModel/RuntimeConfig.cs | 3 + 7 files changed, 347 insertions(+) create mode 100644 src/Config/Converters/RuntimeAutoEntitiesConverter.cs create mode 100644 src/Config/ObjectModel/AutoEntity.cs create mode 100644 src/Config/ObjectModel/AutoEntityPatterns.cs create mode 100644 src/Config/ObjectModel/AutoEntityTemplate.cs create mode 100644 src/Config/ObjectModel/RuntimeAutoEntities.cs diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 80cfd953ad..1ee3677a13 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -693,6 +693,143 @@ } } }, + "autoentities": { + "type": "object", + "description": "Auto-entity definitions for wildcard pattern matching", + "patternProperties": { + "^.*$": { + "type": "object", + "additionalProperties": false, + "properties": { + "patterns": { + "type": "object", + "description": "Pattern matching rules for including/excluding database objects", + "additionalProperties": false, + "properties": { + "include": { + "type": ["string", "null"], + "description": "T-SQL LIKE pattern to include database objects (e.g., '%.%')", + "default": null + }, + "exclude": { + "type": ["string", "null"], + "description": "T-SQL LIKE pattern to exclude database objects (e.g., 'sales.%')", + "default": null + }, + "name": { + "type": ["string", "null"], + "description": "Interpolation syntax for entity naming (must be unique, e.g., '{schema}{object}')", + "default": null + } + } + }, + "template": { + "type": "object", + "description": "Template configuration for generated entities", + "additionalProperties": false, + "properties": { + "mcp": { + "type": "object", + "description": "MCP endpoint configuration", + "additionalProperties": false, + "properties": { + "dml-tool": { + "type": "boolean", + "description": "Enable/disable DML tool", + "default": true + } + } + }, + "rest": { + "type": "object", + "description": "REST endpoint configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable REST endpoint", + "default": true + } + } + }, + "graphql": { + "type": "object", + "description": "GraphQL endpoint configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable GraphQL endpoint", + "default": true + } + } + }, + "health": { + "type": "object", + "description": "Health check configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable health check endpoint", + "default": true + } + } + }, + "cache": { + "type": "object", + "description": "Cache configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable caching", + "default": false + }, + "ttl-seconds": { + "type": ["integer", "null"], + "description": "Time-to-live for cached responses in seconds", + "default": null, + "minimum": 1 + }, + "level": { + "type": ["string", "null"], + "description": "Cache level (L1 or L1L2)", + "enum": ["L1", "L1L2", null], + "default": null + } + } + } + } + }, + "permissions": { + "type": "array", + "description": "Permissions configuration for generated entities (at least one required)", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["role", "actions"], + "properties": { + "role": { + "type": "string", + "description": "Role name" + }, + "actions": { + "type": "array", + "description": "Allowed actions for this role", + "items": { + "$ref": "#/$defs/action" + } + } + } + } + } + }, + "required": ["permissions"] + } + } + }, "entities": { "type": "object", "description": "Entities that will be exposed via REST and/or GraphQL", diff --git a/src/Config/Converters/RuntimeAutoEntitiesConverter.cs b/src/Config/Converters/RuntimeAutoEntitiesConverter.cs new file mode 100644 index 0000000000..bc0abf8ff4 --- /dev/null +++ b/src/Config/Converters/RuntimeAutoEntitiesConverter.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// Custom JSON converter for RuntimeAutoEntities. +/// +class RuntimeAutoEntitiesConverter : JsonConverter +{ + /// + public override RuntimeAutoEntities? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Dictionary? autoEntities = + JsonSerializer.Deserialize>(ref reader, options); + + return new RuntimeAutoEntities(autoEntities); + } + + /// + public override void Write(Utf8JsonWriter writer, RuntimeAutoEntities value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach ((string key, AutoEntity autoEntity) in value) + { + writer.WritePropertyName(key); + JsonSerializer.Serialize(writer, autoEntity, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/ObjectModel/AutoEntity.cs b/src/Config/ObjectModel/AutoEntity.cs new file mode 100644 index 0000000000..b580a8f5c5 --- /dev/null +++ b/src/Config/ObjectModel/AutoEntity.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Defines an individual auto-entity definition with patterns, template, and permissions. +/// +/// Pattern matching rules for including/excluding database objects +/// Template configuration for generated entities +/// Permissions configuration for generated entities (at least one required) +public record AutoEntity( + [property: JsonPropertyName("patterns")] AutoEntityPatterns Patterns, + [property: JsonPropertyName("template")] AutoEntityTemplate Template, + [property: JsonPropertyName("permissions")] EntityPermission[] Permissions +); diff --git a/src/Config/ObjectModel/AutoEntityPatterns.cs b/src/Config/ObjectModel/AutoEntityPatterns.cs new file mode 100644 index 0000000000..037202a48e --- /dev/null +++ b/src/Config/ObjectModel/AutoEntityPatterns.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Defines the pattern matching rules for auto-entities. +/// +/// T-SQL LIKE pattern to include database objects (default: null) +/// T-SQL LIKE pattern to exclude database objects (default: null) +/// Interpolation syntax for entity naming (must be unique, default: null) +public record AutoEntityPatterns( + [property: JsonPropertyName("include")] string? Include = null, + [property: JsonPropertyName("exclude")] string? Exclude = null, + [property: JsonPropertyName("name")] string? Name = null +); diff --git a/src/Config/ObjectModel/AutoEntityTemplate.cs b/src/Config/ObjectModel/AutoEntityTemplate.cs new file mode 100644 index 0000000000..2baefcf52a --- /dev/null +++ b/src/Config/ObjectModel/AutoEntityTemplate.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Defines the template configuration for auto-entities. +/// +/// MCP endpoint configuration +/// REST endpoint configuration +/// GraphQL endpoint configuration +/// Health check configuration +/// Cache configuration +public record AutoEntityTemplate( + [property: JsonPropertyName("mcp")] AutoEntityMcpTemplate? Mcp = null, + [property: JsonPropertyName("rest")] AutoEntityRestTemplate? Rest = null, + [property: JsonPropertyName("graphql")] AutoEntityGraphQLTemplate? GraphQL = null, + [property: JsonPropertyName("health")] AutoEntityHealthTemplate? Health = null, + [property: JsonPropertyName("cache")] EntityCacheOptions? Cache = null +); + +/// +/// MCP template configuration for auto-entities. +/// +/// Enable/disable DML tool (default: true) +public record AutoEntityMcpTemplate( + [property: JsonPropertyName("dml-tool")] bool DmlTool = true +); + +/// +/// REST template configuration for auto-entities. +/// +/// Enable/disable REST endpoint (default: true) +public record AutoEntityRestTemplate( + [property: JsonPropertyName("enabled")] bool Enabled = true +); + +/// +/// GraphQL template configuration for auto-entities. +/// +/// Enable/disable GraphQL endpoint (default: true) +public record AutoEntityGraphQLTemplate( + [property: JsonPropertyName("enabled")] bool Enabled = true +); + +/// +/// Health check template configuration for auto-entities. +/// +/// Enable/disable health check endpoint (default: true) +public record AutoEntityHealthTemplate( + [property: JsonPropertyName("enabled")] bool Enabled = true +); diff --git a/src/Config/ObjectModel/RuntimeAutoEntities.cs b/src/Config/ObjectModel/RuntimeAutoEntities.cs new file mode 100644 index 0000000000..590984c0a9 --- /dev/null +++ b/src/Config/ObjectModel/RuntimeAutoEntities.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.Converters; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Represents a collection of auto-entity definitions. +/// Each definition is keyed by a unique definition name. +/// +[JsonConverter(typeof(RuntimeAutoEntitiesConverter))] +public class RuntimeAutoEntities : IEnumerable> +{ + private readonly Dictionary _autoEntities; + + /// + /// Creates a new RuntimeAutoEntities collection. + /// + /// Dictionary of auto-entity definitions keyed by definition name. + public RuntimeAutoEntities(Dictionary? autoEntities = null) + { + _autoEntities = autoEntities ?? new Dictionary(); + } + + /// + /// Gets an auto-entity definition by its definition name. + /// + /// The name of the auto-entity definition. + /// The auto-entity definition. + public AutoEntity this[string definitionName] => _autoEntities[definitionName]; + + /// + /// Tries to get an auto-entity definition by its definition name. + /// + /// The name of the auto-entity definition. + /// The auto-entity definition if found. + /// True if the auto-entity definition was found, false otherwise. + public bool TryGetValue(string definitionName, [NotNullWhen(true)] out AutoEntity? autoEntity) + { + return _autoEntities.TryGetValue(definitionName, out autoEntity); + } + + /// + /// Determines whether an auto-entity definition with the specified name exists. + /// + /// The name of the auto-entity definition. + /// True if the auto-entity definition exists, false otherwise. + public bool ContainsKey(string definitionName) + { + return _autoEntities.ContainsKey(definitionName); + } + + /// + /// Gets the number of auto-entity definitions in the collection. + /// + public int Count => _autoEntities.Count; + + /// + /// Gets all the auto-entity definition names. + /// + public IEnumerable Keys => _autoEntities.Keys; + + /// + /// Gets all the auto-entity definitions. + /// + public IEnumerable Values => _autoEntities.Values; + + public IEnumerator> GetEnumerator() + { + return _autoEntities.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index a450e1265c..e2905680ae 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -25,6 +25,9 @@ public record RuntimeConfig [JsonPropertyName("azure-key-vault")] public AzureKeyVaultOptions? AzureKeyVault { get; init; } + [JsonPropertyName("autoentities")] + public RuntimeAutoEntities? AutoEntities { get; init; } + public virtual RuntimeEntities Entities { get; init; } public DataSourceFiles? DataSourceFiles { get; init; } From 15e4674276e3e27c8eee561f5ce0541552aeae4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:09:27 +0000 Subject: [PATCH 03/25] Add test for autoentities serialization/deserialization Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> --- .../Configuration/RuntimeConfigLoaderTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs index 7dcf837d08..9997eda401 100644 --- a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs +++ b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs @@ -101,4 +101,37 @@ public async Task FailLoadMultiDataSourceConfigDuplicateEntities(string configPa Assert.IsTrue(error.StartsWith("Deserialization of the configuration file failed during a post-processing step.")); Assert.IsTrue(error.Contains("An item with the same key has already been added.")); } + + /// + /// Test validates that a config file with autoentities section can be loaded successfully. + /// + [TestMethod] + public async Task CanLoadConfigWithAutoEntities() + { + string configPath = "dab-config.AutoEntities.json"; + string fileContents = await File.ReadAllTextAsync(configPath); + + IFileSystem fs = new MockFileSystem(new Dictionary() { { configPath, new MockFileData(fileContents) } }); + + FileSystemRuntimeConfigLoader loader = new(fs); + + Assert.IsTrue(loader.TryLoadConfig(configPath, out RuntimeConfig runtimeConfig), "Failed to load config with autoentities"); + Assert.IsNotNull(runtimeConfig.AutoEntities, "AutoEntities should not be null"); + Assert.AreEqual(2, runtimeConfig.AutoEntities.Count, "Should have 2 auto-entity definitions"); + + // Verify first auto-entity definition + Assert.IsTrue(runtimeConfig.AutoEntities.ContainsKey("all-tables"), "Should contain 'all-tables' definition"); + AutoEntity allTables = runtimeConfig.AutoEntities["all-tables"]; + Assert.AreEqual("%.%", allTables.Patterns.Include, "Include pattern should match"); + Assert.AreEqual("sys.%", allTables.Patterns.Exclude, "Exclude pattern should match"); + Assert.AreEqual("{schema}_{object}", allTables.Patterns.Name, "Name pattern should match"); + Assert.AreEqual(1, allTables.Permissions.Length, "Should have 1 permission"); + + // Verify second auto-entity definition + Assert.IsTrue(runtimeConfig.AutoEntities.ContainsKey("admin-tables"), "Should contain 'admin-tables' definition"); + AutoEntity adminTables = runtimeConfig.AutoEntities["admin-tables"]; + Assert.AreEqual("admin.%", adminTables.Patterns.Include, "Include pattern should match"); + Assert.IsNull(adminTables.Patterns.Exclude, "Exclude pattern should be null"); + Assert.AreEqual(1, adminTables.Permissions.Length, "Should have 1 permission"); + } } From 92fe8a2e1b86e6e47a93ce73be74f8a7cae4611b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:10:32 +0000 Subject: [PATCH 04/25] Add test config file with autoentities Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> --- .../dab-config.AutoEntities.json | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/Service.Tests/dab-config.AutoEntities.json diff --git a/src/Service.Tests/dab-config.AutoEntities.json b/src/Service.Tests/dab-config.AutoEntities.json new file mode 100644 index 0000000000..af9820a7be --- /dev/null +++ b/src/Service.Tests/dab-config.AutoEntities.json @@ -0,0 +1,79 @@ +{ + "$schema": "dab.draft.schema.json", + "data-source": { + "database-type": "mssql", + "connection-string": "Server=localhost;Database=TestDB;User ID=sa;Password=PLACEHOLDER;TrustServerCertificate=true" + }, + "runtime": { + "rest": { + "path": "/api", + "enabled": true + }, + "graphql": { + "path": "/graphql", + "enabled": true + } + }, + "autoentities": { + "all-tables": { + "patterns": { + "include": "%.%", + "exclude": "sys.%", + "name": "{schema}_{object}" + }, + "template": { + "mcp": { + "dml-tool": true + }, + "rest": { + "enabled": true + }, + "graphql": { + "enabled": true + }, + "health": { + "enabled": true + }, + "cache": { + "enabled": false + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": ["read"] + } + ] + }, + "admin-tables": { + "patterns": { + "include": "admin.%" + }, + "template": { + "rest": { + "enabled": true + }, + "graphql": { + "enabled": false + } + }, + "permissions": [ + { + "role": "admin", + "actions": ["*"] + } + ] + } + }, + "entities": { + "Book": { + "source": "books", + "permissions": [ + { + "role": "anonymous", + "actions": ["read"] + } + ] + } + } +} From e02c53da4ee7bce5f63349d41f42b903926dc0c3 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 17 Nov 2025 11:43:59 -0800 Subject: [PATCH 05/25] Changes to schema file --- schemas/dab.draft.schema.json | 136 ++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 24 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 1ee3677a13..0c3fa7b38c 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -707,19 +707,19 @@ "additionalProperties": false, "properties": { "include": { - "type": ["string", "null"], + "type": "string", "description": "T-SQL LIKE pattern to include database objects (e.g., '%.%')", - "default": null + "default": "%.%" }, "exclude": { - "type": ["string", "null"], + "type": "string", "description": "T-SQL LIKE pattern to exclude database objects (e.g., 'sales.%')", "default": null }, "name": { - "type": ["string", "null"], - "description": "Interpolation syntax for entity naming (must be unique, e.g., '{schema}{object}')", - "default": null + "type": "string", + "description": "Interpolation syntax for entity naming, must be unique for every entity inside the pattern", + "default": "{schema}{object}" } } }, @@ -733,10 +733,50 @@ "description": "MCP endpoint configuration", "additionalProperties": false, "properties": { - "dml-tool": { - "type": "boolean", - "description": "Enable/disable DML tool", - "default": true + "dml-tools": { + "oneOf": [ + { + "type": "boolean", + "description": "Enable/disable all DML tools with default settings." + }, + { + "type": "object", + "description": "Individual DML tools configuration", + "additionalProperties": false, + "properties": { + "describe-entities": { + "type": "boolean", + "description": "Enable/disable the describe-entities tool.", + "default": false + }, + "create-record": { + "type": "boolean", + "description": "Enable/disable the create-record tool.", + "default": false + }, + "read-records": { + "type": "boolean", + "description": "Enable/disable the read-records tool.", + "default": false + }, + "update-record": { + "type": "boolean", + "description": "Enable/disable the update-record tool.", + "default": false + }, + "delete-record": { + "type": "boolean", + "description": "Enable/disable the delete-record tool.", + "default": false + }, + "execute-entity": { + "type": "boolean", + "description": "Enable/disable the execute-entity tool.", + "default": false + } + } + } + ] } } }, @@ -787,15 +827,15 @@ "default": false }, "ttl-seconds": { - "type": ["integer", "null"], + "type": [ "integer", "null" ], "description": "Time-to-live for cached responses in seconds", "default": null, "minimum": 1 }, "level": { - "type": ["string", "null"], + "type": "string", "description": "Cache level (L1 or L1L2)", - "enum": ["L1", "L1L2", null], + "enum": [ "L1", "L1L2", null ], "default": null } } @@ -804,27 +844,75 @@ }, "permissions": { "type": "array", - "description": "Permissions configuration for generated entities (at least one required)", - "minItems": 1, + "description": "Permissions assigned to this object", "items": { "type": "object", "additionalProperties": false, - "required": ["role", "actions"], "properties": { "role": { - "type": "string", - "description": "Role name" + "type": "string" }, "actions": { - "type": "array", - "description": "Allowed actions for this role", - "items": { - "$ref": "#/$defs/action" - } + "oneOf": [ + { + "type": "string", + "pattern": "[*]" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/action" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "action": { + "$ref": "#/$defs/action" + }, + "fields": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "policy": { + "type": "object", + "description": "Define item-level security policy", + "additionalProperties": false, + "properties": { + "database": { + "type": "string" + } + } + } + } + } + ] + }, + "uniqueItems": true + } + ] } } - } + }, + "required": [ "role", "actions" ] } + } }, "required": ["permissions"] } From 6cbba87fb86a1da8343a3675b2a9ce98d74efc60 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 18 Nov 2025 11:26:00 -0800 Subject: [PATCH 06/25] Fixes to schema file --- schemas/dab.draft.schema.json | 42 ++++++++--------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 0c3fa7b38c..fb759befb7 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -707,13 +707,19 @@ "additionalProperties": false, "properties": { "include": { - "type": "string", + "type": "array", "description": "T-SQL LIKE pattern to include database objects (e.g., '%.%')", - "default": "%.%" + "items": { + "type": "string" + }, + "default": null }, "exclude": { - "type": "string", + "type": "array", "description": "T-SQL LIKE pattern to exclude database objects (e.g., 'sales.%')", + "items": { + "type": "string" + }, "default": null }, "name": { @@ -871,34 +877,6 @@ "properties": { "action": { "$ref": "#/$defs/action" - }, - "fields": { - "type": "object", - "additionalProperties": false, - "properties": { - "include": { - "type": "array", - "items": { - "type": "string" - } - }, - "exclude": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "policy": { - "type": "object", - "description": "Define item-level security policy", - "additionalProperties": false, - "properties": { - "database": { - "type": "string" - } - } } } } @@ -913,8 +891,6 @@ "required": [ "role", "actions" ] } } - }, - "required": ["permissions"] } } }, From 8c2549375c9ac29888e5bb21f6d2911a6bf34bae Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 20 Nov 2025 16:38:20 -0800 Subject: [PATCH 07/25] Add serialization and deserialization of autoentities --- src/Config/Converters/AutoentityConverter.cs | 100 ++++++++++++ .../Converters/AutoentityPatternsConverter.cs | 152 ++++++++++++++++++ .../Converters/AutoentityTemplateConverter.cs | 125 ++++++++++++++ ...zureLogAnalyticsOptionsConverterFactory.cs | 2 +- .../EntityRestOptionsConverterFactory.cs | 1 + .../RuntimeAutoEntitiesConverter.cs | 19 ++- src/Config/ObjectModel/AutoEntity.cs | 60 ++++++- src/Config/ObjectModel/AutoEntityPatterns.cs | 97 ++++++++++- src/Config/ObjectModel/AutoEntityTemplate.cs | 147 ++++++++++++----- src/Config/ObjectModel/RuntimeAutoEntities.cs | 71 ++------ src/Config/ObjectModel/RuntimeConfig.cs | 5 +- .../Configuration/ConfigurationTests.cs | 125 ++++++++++++++ .../Configuration/RuntimeConfigLoaderTests.cs | 33 ---- .../dab-config.AutoEntities.json | 79 --------- 14 files changed, 780 insertions(+), 236 deletions(-) create mode 100644 src/Config/Converters/AutoentityConverter.cs create mode 100644 src/Config/Converters/AutoentityPatternsConverter.cs create mode 100644 src/Config/Converters/AutoentityTemplateConverter.cs delete mode 100644 src/Service.Tests/dab-config.AutoEntities.json diff --git a/src/Config/Converters/AutoentityConverter.cs b/src/Config/Converters/AutoentityConverter.cs new file mode 100644 index 0000000000..991560b134 --- /dev/null +++ b/src/Config/Converters/AutoentityConverter.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +internal class AutoentityConverter : JsonConverter +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + public AutoentityConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + /// + public override Autoentity? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + // Initialize all sub-properties to null. + AutoentityPatterns? patterns = null; + AutoentityTemplate? template = null; + EntityPermission[]? permissions = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new Autoentity(patterns, template, permissions); + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "patterns": + AutoentityPatternsConverter patternsConverter = new(_replaceEnvVar); + patterns = patternsConverter.Read(ref reader, typeof(AutoentityPatterns), options); + break; + + case "template": + AutoentityTemplateConverter templateConverter = new(_replaceEnvVar); + template = templateConverter.Read(ref reader, typeof(AutoentityTemplate), options); + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + } + + throw new JsonException("Unable to read the Autoentities"); + } + + /// + /// When writing the autoentities back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + /// + public override void Write(Utf8JsonWriter writer, Autoentity value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + AutoentityPatterns? patterns = value?.Patterns; + if (patterns?.UserProvidedIncludeOptions is true + || patterns?.UserProvidedExcludeOptions is true + || patterns?.UserProvidedNameOptions is true) + { + AutoentityPatternsConverter autoentityPatternsConverter = options.GetConverter(typeof(AutoentityPatterns)) as AutoentityPatternsConverter ?? + throw new JsonException("Failed to get autoentities.patterns options converter"); + writer.WritePropertyName("patterns"); + autoentityPatternsConverter.Write(writer, patterns, options); + } + + AutoentityTemplate? template = value?.Template; + if (template?.UserProvidedRestOptions is true + || template?.UserProvidedGraphQLOptions is true + || template?.UserProvidedHealthOptions is true + || template?.UserProvidedCacheOptions is true) + { + AutoentityTemplateConverter autoentityTemplateConverter = options.GetConverter(typeof(AutoentityTemplate)) as AutoentityTemplateConverter ?? + throw new JsonException("Failed to get autoentities.template options converter"); + writer.WritePropertyName("template"); + autoentityTemplateConverter.Write(writer, template, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/Converters/AutoentityPatternsConverter.cs b/src/Config/Converters/AutoentityPatternsConverter.cs new file mode 100644 index 0000000000..534e8468be --- /dev/null +++ b/src/Config/Converters/AutoentityPatternsConverter.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +internal class AutoentityPatternsConverter : JsonConverter +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + public AutoentityPatternsConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + /// + public override AutoentityPatterns? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + string[]? include = null; + string[]? exclude = null; + string? name = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new AutoentityPatterns(include, exclude, name); + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "include": + if (reader.TokenType is not JsonTokenType.Null) + { + List includeList = new(); + + if (reader.TokenType == JsonTokenType.StartObject) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + string? value = reader.DeserializeString(_replaceEnvVar); + if (value is not null) + { + includeList.Add(value); + } + } + + include = includeList.ToArray(); + } + else + { + throw new JsonException("Expected array for 'include' property."); + } + } + + break; + + case "exclude": + if (reader.TokenType is not JsonTokenType.Null) + { + List excludeList = new(); + + if (reader.TokenType == JsonTokenType.StartObject) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + string? value = reader.DeserializeString(_replaceEnvVar); + if (value is not null) + { + excludeList.Add(value); + } + } + + exclude = excludeList.ToArray(); + } + else + { + throw new JsonException("Expected array for 'exclude' property."); + } + } + + break; + + case "name": + name = reader.DeserializeString(_replaceEnvVar); + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + } + + throw new JsonException("Unable to read the Autoentities Pattern Options"); + } + + /// + /// When writing the autoentities.patterns back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + /// + public override void Write(Utf8JsonWriter writer, AutoentityPatterns value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value?.UserProvidedIncludeOptions is true) + { + writer.WritePropertyName("include"); + writer.WriteStartArray(); + foreach (string? include in value.Include) + { + JsonSerializer.Serialize(writer, include, options); + } + + writer.WriteEndArray(); + } + + if (value?.UserProvidedExcludeOptions is true) + { + writer.WritePropertyName("exclude"); + writer.WriteStartArray(); + foreach (string? exclude in value.Exclude) + { + JsonSerializer.Serialize(writer, exclude, options); + } + + writer.WriteEndArray(); + } + + if (value?.UserProvidedNameOptions is true) + { + writer.WritePropertyName("name"); + JsonSerializer.Serialize(writer, value.Name, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs new file mode 100644 index 0000000000..56d43c3a22 --- /dev/null +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +internal class AutoentityTemplateConverter : JsonConverter +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + public AutoentityTemplateConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + /// + public override AutoentityTemplate? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + // Create converters for each of the sub-properties. + EntityRestOptionsConverterFactory restOptionsConverterFactory = new(_replaceEnvVar); + JsonConverter restOptionsConverter = (JsonConverter)(restOptionsConverterFactory.CreateConverter(typeof(EntityRestOptions), options) + ?? throw new JsonException("Unable to create converter for EntityRestOptions")); + + EntityGraphQLOptionsConverterFactory graphQLOptionsConverterFactory = new(_replaceEnvVar); + JsonConverter graphQLOptionsConverter = (JsonConverter)(graphQLOptionsConverterFactory.CreateConverter(typeof(EntityGraphQLOptions), options) + ?? throw new JsonException("Unable to create converter for EntityGraphQLOptions")); + + EntityHealthOptionsConvertorFactory healthOptionsConverterFactory = new(); + JsonConverter healthOptionsConverter = (JsonConverter)(healthOptionsConverterFactory.CreateConverter(typeof(EntityHealthCheckConfig), options) + ?? throw new JsonException("Unable to create converter for EntityHealthCheckConfig")); + + EntityCacheOptionsConverterFactory cacheOptionsConverterFactory = new(_replaceEnvVar); + JsonConverter cacheOptionsConverter = (JsonConverter)(cacheOptionsConverterFactory.CreateConverter(typeof(EntityCacheOptions), options) + ?? throw new JsonException("Unable to create converter for EntityCacheOptions")); + + // Initialize all sub-properties to null. + EntityRestOptions? rest = null; + EntityGraphQLOptions? graphQL = null; + EntityHealthCheckConfig? health = null; + EntityCacheOptions? cache = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new AutoentityTemplate(rest, graphQL, health, cache); + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "rest": + rest = restOptionsConverter.Read(ref reader, typeof(EntityRestOptions), options); + break; + + case "graphql": + graphQL = graphQLOptionsConverter.Read(ref reader, typeof(EntityGraphQLOptions), options); + break; + + case "health": + health = healthOptionsConverter.Read(ref reader, typeof(EntityHealthCheckConfig), options); + break; + + case "cache": + cache = cacheOptionsConverter.Read(ref reader, typeof(EntityCacheOptions), options); + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + } + + throw new JsonException("Unable to read the Autoentities Pattern Options"); + } + + /// + /// When writing the autoentities.template back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + /// + public override void Write(Utf8JsonWriter writer, AutoentityTemplate value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value?.UserProvidedRestOptions is true) + { + writer.WritePropertyName("rest"); + JsonSerializer.Serialize(writer, value.Rest, options); + } + + if (value?.UserProvidedGraphQLOptions is true) + { + writer.WritePropertyName("graphql"); + JsonSerializer.Serialize(writer, value.GraphQL, options); + } + + if (value?.UserProvidedHealthOptions is true) + { + writer.WritePropertyName("health"); + JsonSerializer.Serialize(writer, value.Health, options); + } + + if (value?.UserProvidedCacheOptions is true) + { + writer.WritePropertyName("cache"); + JsonSerializer.Serialize(writer, value.Cache, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs index 3fcbe8c7bd..f5517410d1 100644 --- a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs @@ -144,7 +144,7 @@ public override void Write(Utf8JsonWriter writer, AzureLogAnalyticsOptions value if (value?.Auth is not null && (value.Auth.UserProvidedCustomTableName || value.Auth.UserProvidedDcrImmutableId || value.Auth.UserProvidedDceEndpoint)) { AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = options.GetConverter(typeof(AzureLogAnalyticsAuthOptions)) as AzureLogAnalyticsAuthOptionsConverter ?? - throw new JsonException("Failed to get azure-log-analytics.auth options converter"); + throw new JsonException("Failed to get azure-log-analytics.auth options converter"); writer.WritePropertyName("auth"); authOptionsConverter.Write(writer, value.Auth, options); diff --git a/src/Config/Converters/EntityRestOptionsConverterFactory.cs b/src/Config/Converters/EntityRestOptionsConverterFactory.cs index cc33943caa..d73dab485e 100644 --- a/src/Config/Converters/EntityRestOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityRestOptionsConverterFactory.cs @@ -92,6 +92,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) restOptions = restOptions with { Methods = methods.ToArray() }; break; + case "enabled": reader.Read(); restOptions = restOptions with { Enabled = reader.GetBoolean() }; diff --git a/src/Config/Converters/RuntimeAutoEntitiesConverter.cs b/src/Config/Converters/RuntimeAutoEntitiesConverter.cs index bc0abf8ff4..0c97762f54 100644 --- a/src/Config/Converters/RuntimeAutoEntitiesConverter.cs +++ b/src/Config/Converters/RuntimeAutoEntitiesConverter.cs @@ -1,31 +1,30 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; using Azure.DataApiBuilder.Config.ObjectModel; namespace Azure.DataApiBuilder.Config.Converters; -/// -/// Custom JSON converter for RuntimeAutoEntities. -/// -class RuntimeAutoEntitiesConverter : JsonConverter +class RuntimeAutoentitiesConverter : JsonConverter { /// - public override RuntimeAutoEntities? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override RuntimeAutoentities? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - Dictionary? autoEntities = - JsonSerializer.Deserialize>(ref reader, options); + Dictionary autoEntities = + JsonSerializer.Deserialize>(ref reader, options) ?? + throw new JsonException("Failed to read autoentities"); - return new RuntimeAutoEntities(autoEntities); + return new RuntimeAutoentities(new ReadOnlyDictionary(autoEntities)); } /// - public override void Write(Utf8JsonWriter writer, RuntimeAutoEntities value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, RuntimeAutoentities value, JsonSerializerOptions options) { writer.WriteStartObject(); - foreach ((string key, AutoEntity autoEntity) in value) + foreach ((string key, Autoentity autoEntity) in value.AutoEntities) { writer.WritePropertyName(key); JsonSerializer.Serialize(writer, autoEntity, options); diff --git a/src/Config/ObjectModel/AutoEntity.cs b/src/Config/ObjectModel/AutoEntity.cs index b580a8f5c5..b5455c84ba 100644 --- a/src/Config/ObjectModel/AutoEntity.cs +++ b/src/Config/ObjectModel/AutoEntity.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Azure.DataApiBuilder.Config.ObjectModel; @@ -11,8 +12,57 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// Pattern matching rules for including/excluding database objects /// Template configuration for generated entities /// Permissions configuration for generated entities (at least one required) -public record AutoEntity( - [property: JsonPropertyName("patterns")] AutoEntityPatterns Patterns, - [property: JsonPropertyName("template")] AutoEntityTemplate Template, - [property: JsonPropertyName("permissions")] EntityPermission[] Permissions -); +public record Autoentity +{ + public AutoentityPatterns Patterns { get; init; } + public AutoentityTemplate Template { get; init; } + public EntityPermission[] Permissions { get; init; } + + [JsonConstructor] + public Autoentity( + AutoentityPatterns? Patterns, + AutoentityTemplate? Template, + EntityPermission[]? Permissions) + { + if (Patterns is not null) + { + this.Patterns = Patterns; + } + else + { + this.Patterns = new AutoentityPatterns(); + } + + if (Template is not null) + { + this.Template = Template; + } + else + { + this.Template = new AutoentityTemplate(); + } + + if (Permissions is not null) + { + this.Permissions = Permissions; + } + else + { + this.Permissions = Array.Empty(); + } + } + + /// + /// Flag which informs CLI and JSON serializer whether to write permissions + /// property and value to the runtime config file. + /// When user doesn't provide the permissions property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a permissions + /// property/value specified would be interpreted by DAB as "user explicitly set permissions." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Permissions))] + public bool UserProvidedPermissionsOptions { get; init; } = false; +} diff --git a/src/Config/ObjectModel/AutoEntityPatterns.cs b/src/Config/ObjectModel/AutoEntityPatterns.cs index 037202a48e..d287c56109 100644 --- a/src/Config/ObjectModel/AutoEntityPatterns.cs +++ b/src/Config/ObjectModel/AutoEntityPatterns.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Azure.DataApiBuilder.Config.ObjectModel; @@ -8,11 +9,91 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// /// Defines the pattern matching rules for auto-entities. /// -/// T-SQL LIKE pattern to include database objects (default: null) -/// T-SQL LIKE pattern to exclude database objects (default: null) -/// Interpolation syntax for entity naming (must be unique, default: null) -public record AutoEntityPatterns( - [property: JsonPropertyName("include")] string? Include = null, - [property: JsonPropertyName("exclude")] string? Exclude = null, - [property: JsonPropertyName("name")] string? Name = null -); +/// T-SQL LIKE pattern to include database objects +/// T-SQL LIKE pattern to exclude database objects +/// Interpolation syntax for entity naming (must be unique for each generated entity) +public record AutoentityPatterns +{ + public string[] Include { get; init; } + public string[] Exclude { get; init; } + public string Name { get; init; } + + [JsonConstructor] + public AutoentityPatterns( + string[]? Include = null, + string[]? Exclude = null, + string? Name = null) + { + if (Include is not null) + { + this.Include = Include; + UserProvidedIncludeOptions = true; + } + else + { + this.Include = ["%.%"]; + } + + if (Exclude is not null) + { + this.Exclude = Exclude; + UserProvidedExcludeOptions = true; + } + else + { + this.Exclude = []; + } + + if (!string.IsNullOrWhiteSpace(Name)) + { + this.Name = Name; + UserProvidedNameOptions = true; + } + else + { + this.Name = "{object}"; + } + } + + /// + /// Flag which informs CLI and JSON serializer whether to write include + /// property and value to the runtime config file. + /// When user doesn't provide the include property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a include + /// property/value specified would be interpreted by DAB as "user explicitly set include." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Include))] + public bool UserProvidedIncludeOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write exclude + /// property and value to the runtime config file. + /// When user doesn't provide the exclude property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a exclude + /// property/value specified would be interpreted by DAB as "user explicitly set exclude." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Exclude))] + public bool UserProvidedExcludeOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write name + /// property and value to the runtime config file. + /// When user doesn't provide the name property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a name + /// property/value specified would be interpreted by DAB as "user explicitly set name." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Name))] + public bool UserProvidedNameOptions { get; init; } = false; +} diff --git a/src/Config/ObjectModel/AutoEntityTemplate.cs b/src/Config/ObjectModel/AutoEntityTemplate.cs index 2baefcf52a..78e7a423a0 100644 --- a/src/Config/ObjectModel/AutoEntityTemplate.cs +++ b/src/Config/ObjectModel/AutoEntityTemplate.cs @@ -1,54 +1,129 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Azure.DataApiBuilder.Config.ObjectModel; /// -/// Defines the template configuration for auto-entities. +/// Template used by auto-entities to configure all entities it generates. /// /// MCP endpoint configuration /// REST endpoint configuration /// GraphQL endpoint configuration /// Health check configuration /// Cache configuration -public record AutoEntityTemplate( - [property: JsonPropertyName("mcp")] AutoEntityMcpTemplate? Mcp = null, - [property: JsonPropertyName("rest")] AutoEntityRestTemplate? Rest = null, - [property: JsonPropertyName("graphql")] AutoEntityGraphQLTemplate? GraphQL = null, - [property: JsonPropertyName("health")] AutoEntityHealthTemplate? Health = null, - [property: JsonPropertyName("cache")] EntityCacheOptions? Cache = null -); +public record AutoentityTemplate +{ + // TODO: Will add Mcp variable once MCP is supported at an entity level + // public EntityMcpOptions? Mcp { get; init; } + public EntityRestOptions Rest { get; init; } + public EntityGraphQLOptions GraphQL { get; init; } + public EntityHealthCheckConfig Health { get; init; } + public EntityCacheOptions Cache { get; init; } -/// -/// MCP template configuration for auto-entities. -/// -/// Enable/disable DML tool (default: true) -public record AutoEntityMcpTemplate( - [property: JsonPropertyName("dml-tool")] bool DmlTool = true -); + [JsonConstructor] + public AutoentityTemplate( + EntityRestOptions? Rest = null, + EntityGraphQLOptions? GraphQL = null, + EntityHealthCheckConfig? Health = null, + EntityCacheOptions? Cache = null) + { + if (Rest is not null) + { + this.Rest = Rest; + UserProvidedRestOptions = true; + } + else + { + this.Rest = new EntityRestOptions(); + } -/// -/// REST template configuration for auto-entities. -/// -/// Enable/disable REST endpoint (default: true) -public record AutoEntityRestTemplate( - [property: JsonPropertyName("enabled")] bool Enabled = true -); + if (GraphQL is not null) + { + this.GraphQL = GraphQL; + UserProvidedGraphQLOptions = true; + } + else + { + this.GraphQL = new EntityGraphQLOptions(string.Empty, string.Empty); + } -/// -/// GraphQL template configuration for auto-entities. -/// -/// Enable/disable GraphQL endpoint (default: true) -public record AutoEntityGraphQLTemplate( - [property: JsonPropertyName("enabled")] bool Enabled = true -); + if (Health is not null) + { + this.Health = Health; + UserProvidedHealthOptions = true; + } + else + { + this.Health = new EntityHealthCheckConfig(); + } -/// -/// Health check template configuration for auto-entities. -/// -/// Enable/disable health check endpoint (default: true) -public record AutoEntityHealthTemplate( - [property: JsonPropertyName("enabled")] bool Enabled = true -); + if (Cache is not null) + { + this.Cache = Cache; + UserProvidedCacheOptions = true; + } + else + { + this.Cache = new EntityCacheOptions(Enabled: true); + } + } + + /// + /// Flag which informs CLI and JSON serializer whether to write rest + /// property and value to the runtime config file. + /// When user doesn't provide the rest property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a rest + /// property/value specified would be interpreted by DAB as "user explicitly set rest." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Rest))] + public bool UserProvidedRestOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write graphql + /// property and value to the runtime config file. + /// When user doesn't provide the graphql property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a graphql + /// property/value specified would be interpreted by DAB as "user explicitly set graphql." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(GraphQL))] + public bool UserProvidedGraphQLOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write health + /// property and value to the runtime config file. + /// When user doesn't provide the health property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a health + /// property/value specified would be interpreted by DAB as "user explicitly set health." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Health))] + public bool UserProvidedHealthOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write cache + /// property and value to the runtime config file. + /// When user doesn't provide the cache property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a cache + /// property/value specified would be interpreted by DAB as "user explicitly set cache." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Cache))] + public bool UserProvidedCacheOptions { get; init; } = false; +} diff --git a/src/Config/ObjectModel/RuntimeAutoEntities.cs b/src/Config/ObjectModel/RuntimeAutoEntities.cs index 590984c0a9..7879078ee1 100644 --- a/src/Config/ObjectModel/RuntimeAutoEntities.cs +++ b/src/Config/ObjectModel/RuntimeAutoEntities.cs @@ -1,81 +1,28 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Azure.DataApiBuilder.Config.Converters; namespace Azure.DataApiBuilder.Config.ObjectModel; /// -/// Represents a collection of auto-entity definitions. -/// Each definition is keyed by a unique definition name. +/// Represents a collection of available from the RuntimeConfig. /// -[JsonConverter(typeof(RuntimeAutoEntitiesConverter))] -public class RuntimeAutoEntities : IEnumerable> +[JsonConverter(typeof(RuntimeAutoentitiesConverter))] +public record RuntimeAutoentities { - private readonly Dictionary _autoEntities; - - /// - /// Creates a new RuntimeAutoEntities collection. - /// - /// Dictionary of auto-entity definitions keyed by definition name. - public RuntimeAutoEntities(Dictionary? autoEntities = null) - { - _autoEntities = autoEntities ?? new Dictionary(); - } - - /// - /// Gets an auto-entity definition by its definition name. - /// - /// The name of the auto-entity definition. - /// The auto-entity definition. - public AutoEntity this[string definitionName] => _autoEntities[definitionName]; - - /// - /// Tries to get an auto-entity definition by its definition name. - /// - /// The name of the auto-entity definition. - /// The auto-entity definition if found. - /// True if the auto-entity definition was found, false otherwise. - public bool TryGetValue(string definitionName, [NotNullWhen(true)] out AutoEntity? autoEntity) - { - return _autoEntities.TryGetValue(definitionName, out autoEntity); - } - /// - /// Determines whether an auto-entity definition with the specified name exists. + /// The collection of available from the RuntimeConfig. /// - /// The name of the auto-entity definition. - /// True if the auto-entity definition exists, false otherwise. - public bool ContainsKey(string definitionName) - { - return _autoEntities.ContainsKey(definitionName); - } + public IReadOnlyDictionary AutoEntities { get; init; } /// - /// Gets the number of auto-entity definitions in the collection. + /// Creates a new instance of the class using a collection of entities. /// - public int Count => _autoEntities.Count; - - /// - /// Gets all the auto-entity definition names. - /// - public IEnumerable Keys => _autoEntities.Keys; - - /// - /// Gets all the auto-entity definitions. - /// - public IEnumerable Values => _autoEntities.Values; - - public IEnumerator> GetEnumerator() - { - return _autoEntities.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() + /// The collection of auto-entities to map to RuntimeAutoentities. + public RuntimeAutoentities(IReadOnlyDictionary autoEntities) { - return GetEnumerator(); + AutoEntities = autoEntities; } } diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index e2905680ae..0a074be2c0 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -25,8 +25,7 @@ public record RuntimeConfig [JsonPropertyName("azure-key-vault")] public AzureKeyVaultOptions? AzureKeyVault { get; init; } - [JsonPropertyName("autoentities")] - public RuntimeAutoEntities? AutoEntities { get; init; } + public RuntimeAutoentities? Autoentities { get; init; } public virtual RuntimeEntities Entities { get; init; } @@ -249,6 +248,7 @@ public RuntimeConfig( string? Schema, DataSource DataSource, RuntimeEntities Entities, + RuntimeAutoentities? Autoentities = null, RuntimeOptions? Runtime = null, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null) @@ -258,6 +258,7 @@ public RuntimeConfig( this.Runtime = Runtime; this.AzureKeyVault = AzureKeyVault; this.Entities = Entities; + this.Autoentities = Autoentities; this.DefaultDataSourceName = Guid.NewGuid().ToString(); if (this.DataSource is null) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 65f6e6643b..7eacc8ee1a 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4271,6 +4271,131 @@ public void FileSinkSerialization( } } + /// + /// Test validates that autoentities section can be deserialized and serialized correctly. + /// + [TestMethod] + [TestCategory(TestCategory.MSSQL)] + public void TestAutoEntitiesSerializationDeserialization( + string[]? include, + string[]? exclude, + string? name, + bool? restEnabled, + bool? graphqlEnabled, + bool? healthCheckEnabled, + bool? cacheEnabled, + int? cacheTTL, + EntityCacheLevel? cacheLevel, + string role, + EntityAction[] entityActions) + { + TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); + + Dictionary createdAutoentity = new(); + createdAutoentity.Add("test-entity", + new AutoEntity( + Patterns: new AutoEntityPatterns(include, exclude, name), + Template: new AutoEntityTemplate( + Rest: restEnabled == null ? null : new EntityRestOptions(Enabled: (bool)restEnabled), + GraphQL: graphqlEnabled == null ? null : new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: (bool)graphqlEnabled), + Health: new EntityHealthCheckConfig(healthCheckEnabled), + Cache: new EntityCacheOptions(Enabled: cacheEnabled, TtlSeconds: cacheTTL, Level: cacheLevel) + ), + Permissions: new EntityPermission[1])); + createdAutoentity["test-entity"].Permissions[0] = new EntityPermission(role, entityActions); + RuntimeAutoEntities autoentities = new(createdAutoentity); + + FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader(); + baseLoader.TryLoadKnownConfig(out RuntimeConfig? baseConfig); + + RuntimeConfig config = new( + Schema: baseConfig!.Schema, + DataSource: baseConfig.DataSource, + Runtime: new( + Rest: new(), + GraphQL: new(), + Mcp: new(), + Host: new(null, null), + Telemetry: new() + ), + Entities: baseConfig.Entities, + Autoentities: autoentities + ); + + string configWithCustomJson = config.ToJson(); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configWithCustomJson, out RuntimeConfig? deserializedRuntimeConfig)); + + string serializedConfig = deserializedRuntimeConfig.ToJson(); + + using (JsonDocument parsedDocument = JsonDocument.Parse(serializedConfig)) + { + JsonElement root = parsedDocument.RootElement; + JsonElement autoentitiesElement = root.GetProperty("autoentities"); + + // Validate patterns properties and their values exists in autoentities + JsonElement patternsElement = autoentitiesElement.GetProperty("patterns"); + + bool includeExists = patternsElement.TryGetProperty("include", out JsonElement includeElement); + Assert.AreEqual(expected: true, actual: includeExists); + Assert.AreEqual(expected: include, actual: includeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); + + bool excludeExists = patternsElement.TryGetProperty("exclude", out JsonElement excludeElement); + Assert.AreEqual(expected: (exclude != null), actual: excludeExists); + if (excludeExists) + { + Assert.AreEqual(expected: exclude, actual: excludeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); + } + + bool nameExists = patternsElement.TryGetProperty("name", out JsonElement nameElement); + Assert.AreEqual(expected: true, actual: nameExists); + Assert.AreEqual(expected: name, actual: nameElement.GetString()); + + // Validate template properties and their values exists in autoentities + JsonElement templateElement = autoentitiesElement.GetProperty("template"); + + bool restPropertyExists = templateElement.TryGetProperty("rest", out JsonElement restElement); + Assert.AreEqual(expected: (restEnabled != null), actual: restPropertyExists); + if (restEnabled != null) + { + Assert.IsTrue(restElement.TryGetProperty("enabled", out JsonElement restEnabledElement)); + Assert.AreEqual(expected: restEnabled, actual: restEnabledElement.GetBoolean()); + } + + bool graphqlPropertyExists = templateElement.TryGetProperty("graphql", out JsonElement graphqlElement); + Assert.AreEqual(expected: (graphqlEnabled != null), actual: graphqlPropertyExists); + if (graphqlEnabled != null) + { + Assert.IsTrue(graphqlElement.TryGetProperty("enabled", out JsonElement graphqlEnabledElement)); + Assert.AreEqual(expected: graphqlEnabled, actual: graphqlEnabledElement.GetBoolean()); + } + + bool healthPropertyExists = templateElement.TryGetProperty("health", out JsonElement healthElement); + Assert.AreEqual(expected: true, actual: healthPropertyExists); + Assert.IsTrue(healthElement.TryGetProperty("enabled", out JsonElement healthEnabledElement)); + Assert.AreEqual(expected: (healthCheckEnabled ?? true), actual: healthEnabledElement.GetBoolean()); + + bool cachePropertyExists = templateElement.TryGetProperty("cache", out JsonElement cacheElement); + Assert.AreEqual(expected: true, actual: cachePropertyExists); + Assert.IsTrue(cacheElement.TryGetProperty("enabled", out JsonElement cacheEnabledElement)); + Assert.AreEqual(expected: (cacheEnabled ?? false), actual: cacheEnabledElement.GetBoolean()); + Assert.IsTrue(cacheElement.TryGetProperty("ttl", out JsonElement cacheTtlElement)); + Assert.AreEqual(expected: (cacheTTL ?? EntityCacheOptions.DEFAULT_TTL_SECONDS), actual: cacheTtlElement.GetInt32()); + Assert.IsTrue(cacheElement.TryGetProperty("level", out JsonElement cacheLevelElement)); + Assert.AreEqual(expected: (cacheLevel ?? EntityCacheOptions.DEFAULT_LEVEL).ToString(), actual: cacheLevelElement.GetString()); + + // Validate permissions properties and their values exists in autoentities + JsonElement permissionsElement = autoentitiesElement.GetProperty("permissions"); + + bool roleExists = permissionsElement.TryGetProperty("role", out JsonElement roleElement); + Assert.AreEqual(expected: true, actual: roleExists); + Assert.AreEqual(expected: role, actual: roleElement.GetString()); + + bool entityActionsExists = permissionsElement.TryGetProperty("entity-actions", out JsonElement entityActionsElement); + Assert.AreEqual(expected: true, actual: entityActionsExists); + Assert.AreEqual(expected: entityActions.Select(e => e.ToString()).ToArray(), actual: entityActionsElement.EnumerateArray().Select(e => e.ToString()).ToArray()); + } + } + #nullable disable /// diff --git a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs index 9997eda401..7dcf837d08 100644 --- a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs +++ b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs @@ -101,37 +101,4 @@ public async Task FailLoadMultiDataSourceConfigDuplicateEntities(string configPa Assert.IsTrue(error.StartsWith("Deserialization of the configuration file failed during a post-processing step.")); Assert.IsTrue(error.Contains("An item with the same key has already been added.")); } - - /// - /// Test validates that a config file with autoentities section can be loaded successfully. - /// - [TestMethod] - public async Task CanLoadConfigWithAutoEntities() - { - string configPath = "dab-config.AutoEntities.json"; - string fileContents = await File.ReadAllTextAsync(configPath); - - IFileSystem fs = new MockFileSystem(new Dictionary() { { configPath, new MockFileData(fileContents) } }); - - FileSystemRuntimeConfigLoader loader = new(fs); - - Assert.IsTrue(loader.TryLoadConfig(configPath, out RuntimeConfig runtimeConfig), "Failed to load config with autoentities"); - Assert.IsNotNull(runtimeConfig.AutoEntities, "AutoEntities should not be null"); - Assert.AreEqual(2, runtimeConfig.AutoEntities.Count, "Should have 2 auto-entity definitions"); - - // Verify first auto-entity definition - Assert.IsTrue(runtimeConfig.AutoEntities.ContainsKey("all-tables"), "Should contain 'all-tables' definition"); - AutoEntity allTables = runtimeConfig.AutoEntities["all-tables"]; - Assert.AreEqual("%.%", allTables.Patterns.Include, "Include pattern should match"); - Assert.AreEqual("sys.%", allTables.Patterns.Exclude, "Exclude pattern should match"); - Assert.AreEqual("{schema}_{object}", allTables.Patterns.Name, "Name pattern should match"); - Assert.AreEqual(1, allTables.Permissions.Length, "Should have 1 permission"); - - // Verify second auto-entity definition - Assert.IsTrue(runtimeConfig.AutoEntities.ContainsKey("admin-tables"), "Should contain 'admin-tables' definition"); - AutoEntity adminTables = runtimeConfig.AutoEntities["admin-tables"]; - Assert.AreEqual("admin.%", adminTables.Patterns.Include, "Include pattern should match"); - Assert.IsNull(adminTables.Patterns.Exclude, "Exclude pattern should be null"); - Assert.AreEqual(1, adminTables.Permissions.Length, "Should have 1 permission"); - } } diff --git a/src/Service.Tests/dab-config.AutoEntities.json b/src/Service.Tests/dab-config.AutoEntities.json deleted file mode 100644 index af9820a7be..0000000000 --- a/src/Service.Tests/dab-config.AutoEntities.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "$schema": "dab.draft.schema.json", - "data-source": { - "database-type": "mssql", - "connection-string": "Server=localhost;Database=TestDB;User ID=sa;Password=PLACEHOLDER;TrustServerCertificate=true" - }, - "runtime": { - "rest": { - "path": "/api", - "enabled": true - }, - "graphql": { - "path": "/graphql", - "enabled": true - } - }, - "autoentities": { - "all-tables": { - "patterns": { - "include": "%.%", - "exclude": "sys.%", - "name": "{schema}_{object}" - }, - "template": { - "mcp": { - "dml-tool": true - }, - "rest": { - "enabled": true - }, - "graphql": { - "enabled": true - }, - "health": { - "enabled": true - }, - "cache": { - "enabled": false - } - }, - "permissions": [ - { - "role": "anonymous", - "actions": ["read"] - } - ] - }, - "admin-tables": { - "patterns": { - "include": "admin.%" - }, - "template": { - "rest": { - "enabled": true - }, - "graphql": { - "enabled": false - } - }, - "permissions": [ - { - "role": "admin", - "actions": ["*"] - } - ] - } - }, - "entities": { - "Book": { - "source": "books", - "permissions": [ - { - "role": "anonymous", - "actions": ["read"] - } - ] - } - } -} From db3d137e7dd2154a6cbffb124b8a545b6cf2eb64 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 20 Nov 2025 19:42:25 -0800 Subject: [PATCH 08/25] Fix merge conflicts --- src/Config/Converters/AutoentityConverter.cs | 17 ++++++++--------- .../Converters/AutoentityPatternsConverter.cs | 19 +++++++++---------- .../Converters/AutoentityTemplateConverter.cs | 19 +++++++++---------- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/Config/Converters/AutoentityConverter.cs b/src/Config/Converters/AutoentityConverter.cs index 991560b134..ccd93655c3 100644 --- a/src/Config/Converters/AutoentityConverter.cs +++ b/src/Config/Converters/AutoentityConverter.cs @@ -9,15 +9,14 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class AutoentityConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AutoentityConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AutoentityConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -43,12 +42,12 @@ public AutoentityConverter(bool replaceEnvVar) switch (propertyName) { case "patterns": - AutoentityPatternsConverter patternsConverter = new(_replaceEnvVar); + AutoentityPatternsConverter patternsConverter = new(_replacementSettings); patterns = patternsConverter.Read(ref reader, typeof(AutoentityPatterns), options); break; case "template": - AutoentityTemplateConverter templateConverter = new(_replaceEnvVar); + AutoentityTemplateConverter templateConverter = new(_replacementSettings); template = templateConverter.Read(ref reader, typeof(AutoentityTemplate), options); break; diff --git a/src/Config/Converters/AutoentityPatternsConverter.cs b/src/Config/Converters/AutoentityPatternsConverter.cs index 534e8468be..864ee82abe 100644 --- a/src/Config/Converters/AutoentityPatternsConverter.cs +++ b/src/Config/Converters/AutoentityPatternsConverter.cs @@ -9,15 +9,14 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class AutoentityPatternsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AutoentityPatternsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AutoentityPatternsConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -50,7 +49,7 @@ public AutoentityPatternsConverter(bool replaceEnvVar) { while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - string? value = reader.DeserializeString(_replaceEnvVar); + string? value = reader.DeserializeString(_replacementSettings); if (value is not null) { includeList.Add(value); @@ -76,7 +75,7 @@ public AutoentityPatternsConverter(bool replaceEnvVar) { while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - string? value = reader.DeserializeString(_replaceEnvVar); + string? value = reader.DeserializeString(_replacementSettings); if (value is not null) { excludeList.Add(value); @@ -94,7 +93,7 @@ public AutoentityPatternsConverter(bool replaceEnvVar) break; case "name": - name = reader.DeserializeString(_replaceEnvVar); + name = reader.DeserializeString(_replacementSettings); break; default: diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs index 56d43c3a22..d2c890205d 100644 --- a/src/Config/Converters/AutoentityTemplateConverter.cs +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -9,15 +9,14 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class AutoentityTemplateConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AutoentityTemplateConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -26,11 +25,11 @@ public AutoentityTemplateConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.StartObject) { // Create converters for each of the sub-properties. - EntityRestOptionsConverterFactory restOptionsConverterFactory = new(_replaceEnvVar); + EntityRestOptionsConverterFactory restOptionsConverterFactory = new(_replacementSettings); JsonConverter restOptionsConverter = (JsonConverter)(restOptionsConverterFactory.CreateConverter(typeof(EntityRestOptions), options) ?? throw new JsonException("Unable to create converter for EntityRestOptions")); - EntityGraphQLOptionsConverterFactory graphQLOptionsConverterFactory = new(_replaceEnvVar); + EntityGraphQLOptionsConverterFactory graphQLOptionsConverterFactory = new(_replacementSettings); JsonConverter graphQLOptionsConverter = (JsonConverter)(graphQLOptionsConverterFactory.CreateConverter(typeof(EntityGraphQLOptions), options) ?? throw new JsonException("Unable to create converter for EntityGraphQLOptions")); @@ -38,7 +37,7 @@ public AutoentityTemplateConverter(bool replaceEnvVar) JsonConverter healthOptionsConverter = (JsonConverter)(healthOptionsConverterFactory.CreateConverter(typeof(EntityHealthCheckConfig), options) ?? throw new JsonException("Unable to create converter for EntityHealthCheckConfig")); - EntityCacheOptionsConverterFactory cacheOptionsConverterFactory = new(_replaceEnvVar); + EntityCacheOptionsConverterFactory cacheOptionsConverterFactory = new(_replacementSettings); JsonConverter cacheOptionsConverter = (JsonConverter)(cacheOptionsConverterFactory.CreateConverter(typeof(EntityCacheOptions), options) ?? throw new JsonException("Unable to create converter for EntityCacheOptions")); From a443ae028fd1b0ffcfe0ad16c9cd48c2e2e2c50d Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 24 Nov 2025 12:11:13 -0800 Subject: [PATCH 09/25] Fix tests --- src/Service.Tests/Configuration/ConfigurationTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 681ed9f1af..5b9e6eb9c1 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4306,11 +4306,11 @@ public void TestAutoEntitiesSerializationDeserialization( { TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); - Dictionary createdAutoentity = new(); + Dictionary createdAutoentity = new(); createdAutoentity.Add("test-entity", - new AutoEntity( - Patterns: new AutoEntityPatterns(include, exclude, name), - Template: new AutoEntityTemplate( + new Autoentity( + Patterns: new AutoentityPatterns(include, exclude, name), + Template: new AutoentityTemplate( Rest: restEnabled == null ? null : new EntityRestOptions(Enabled: (bool)restEnabled), GraphQL: graphqlEnabled == null ? null : new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: (bool)graphqlEnabled), Health: new EntityHealthCheckConfig(healthCheckEnabled), @@ -4318,7 +4318,7 @@ public void TestAutoEntitiesSerializationDeserialization( ), Permissions: new EntityPermission[1])); createdAutoentity["test-entity"].Permissions[0] = new EntityPermission(role, entityActions); - RuntimeAutoEntities autoentities = new(createdAutoentity); + RuntimeAutoentities autoentities = new(createdAutoentity); FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader(); baseLoader.TryLoadKnownConfig(out RuntimeConfig? baseConfig); From 853e3560a0c79554768a468e045b1867ca32c945 Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:13:05 -0800 Subject: [PATCH 10/25] Rename RuntimeAutoEntitiesConverter.cs to RuntimeAutoentitiesConverter.cs --- ...meAutoEntitiesConverter.cs => RuntimeAutoentitiesConverter.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Config/Converters/{RuntimeAutoEntitiesConverter.cs => RuntimeAutoentitiesConverter.cs} (100%) diff --git a/src/Config/Converters/RuntimeAutoEntitiesConverter.cs b/src/Config/Converters/RuntimeAutoentitiesConverter.cs similarity index 100% rename from src/Config/Converters/RuntimeAutoEntitiesConverter.cs rename to src/Config/Converters/RuntimeAutoentitiesConverter.cs From 30ac9d6e392c0963cb7ac0c5a530931181328f31 Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:13:31 -0800 Subject: [PATCH 11/25] Rename AutoEntity.cs to Autoentity.cs --- src/Config/ObjectModel/{AutoEntity.cs => Autoentity.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Config/ObjectModel/{AutoEntity.cs => Autoentity.cs} (100%) diff --git a/src/Config/ObjectModel/AutoEntity.cs b/src/Config/ObjectModel/Autoentity.cs similarity index 100% rename from src/Config/ObjectModel/AutoEntity.cs rename to src/Config/ObjectModel/Autoentity.cs From 48c48f4ade69d5955b498d1d80c823579dd86daf Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:14:28 -0800 Subject: [PATCH 12/25] Rename RuntimeAutoEntities.cs to RuntimeAutoentities.cs --- .../{RuntimeAutoEntities.cs => RuntimeAutoentities.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Config/ObjectModel/{RuntimeAutoEntities.cs => RuntimeAutoentities.cs} (100%) diff --git a/src/Config/ObjectModel/RuntimeAutoEntities.cs b/src/Config/ObjectModel/RuntimeAutoentities.cs similarity index 100% rename from src/Config/ObjectModel/RuntimeAutoEntities.cs rename to src/Config/ObjectModel/RuntimeAutoentities.cs From 1dcfbedf608851570a4b5281f1d949e2d836f352 Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:14:54 -0800 Subject: [PATCH 13/25] Rename AutoEntityPatterns.cs to AutoentityPatterns.cs --- .../ObjectModel/{AutoEntityPatterns.cs => AutoentityPatterns.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Config/ObjectModel/{AutoEntityPatterns.cs => AutoentityPatterns.cs} (100%) diff --git a/src/Config/ObjectModel/AutoEntityPatterns.cs b/src/Config/ObjectModel/AutoentityPatterns.cs similarity index 100% rename from src/Config/ObjectModel/AutoEntityPatterns.cs rename to src/Config/ObjectModel/AutoentityPatterns.cs From 7be3a41d7093d77432cf88e10ce8236fb0e4b118 Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:15:27 -0800 Subject: [PATCH 14/25] Rename AutoEntityTemplate.cs to AutoentityTemplate.cs --- .../ObjectModel/{AutoEntityTemplate.cs => AutoentityTemplate.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Config/ObjectModel/{AutoEntityTemplate.cs => AutoentityTemplate.cs} (100%) diff --git a/src/Config/ObjectModel/AutoEntityTemplate.cs b/src/Config/ObjectModel/AutoentityTemplate.cs similarity index 100% rename from src/Config/ObjectModel/AutoEntityTemplate.cs rename to src/Config/ObjectModel/AutoentityTemplate.cs From 5ce410785a782ad5650771e747ead50244f4e408 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 26 Nov 2025 11:29:40 -0800 Subject: [PATCH 15/25] Changes based on comments --- schemas/dab.draft.schema.json | 20 +++++++++---------- .../Converters/AutoentityTemplateConverter.cs | 4 ++++ .../RuntimeAutoentitiesConverter.cs | 5 +++++ src/Config/ObjectModel/AutoentityTemplate.cs | 2 +- src/Config/ObjectModel/RuntimeAutoentities.cs | 2 +- .../Configuration/ConfigurationTests.cs | 12 ++++++----- 6 files changed, 28 insertions(+), 17 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index fb759befb7..c5505a476a 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -695,7 +695,7 @@ }, "autoentities": { "type": "object", - "description": "Auto-entity definitions for wildcard pattern matching", + "description": "Auto-entity definitions for pattern matching", "patternProperties": { "^.*$": { "type": "object", @@ -712,7 +712,7 @@ "items": { "type": "string" }, - "default": null + "default": [ "%.%" ] }, "exclude": { "type": "array", @@ -725,7 +725,7 @@ "name": { "type": "string", "description": "Interpolation syntax for entity naming, must be unique for every entity inside the pattern", - "default": "{schema}{object}" + "default": "{object}" } } }, @@ -753,32 +753,32 @@ "describe-entities": { "type": "boolean", "description": "Enable/disable the describe-entities tool.", - "default": false + "default": true }, "create-record": { "type": "boolean", "description": "Enable/disable the create-record tool.", - "default": false + "default": true }, "read-records": { "type": "boolean", "description": "Enable/disable the read-records tool.", - "default": false + "default": true }, "update-record": { "type": "boolean", "description": "Enable/disable the update-record tool.", - "default": false + "default": true }, "delete-record": { "type": "boolean", "description": "Enable/disable the delete-record tool.", - "default": false + "default": true }, "execute-entity": { "type": "boolean", "description": "Enable/disable the execute-entity tool.", - "default": false + "default": true } } } @@ -842,7 +842,7 @@ "type": "string", "description": "Cache level (L1 or L1L2)", "enum": [ "L1", "L1L2", null ], - "default": null + "default": "L1L2" } } } diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs index d2c890205d..78917b6072 100644 --- a/src/Config/Converters/AutoentityTemplateConverter.cs +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -67,6 +67,10 @@ public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? r graphQL = graphQLOptionsConverter.Read(ref reader, typeof(EntityGraphQLOptions), options); break; + case "mcp": + // TODO: Add MCP support for autoentities needed. + break; + case "health": health = healthOptionsConverter.Read(ref reader, typeof(EntityHealthCheckConfig), options); break; diff --git a/src/Config/Converters/RuntimeAutoentitiesConverter.cs b/src/Config/Converters/RuntimeAutoentitiesConverter.cs index 0c97762f54..b65bcb9989 100644 --- a/src/Config/Converters/RuntimeAutoentitiesConverter.cs +++ b/src/Config/Converters/RuntimeAutoentitiesConverter.cs @@ -8,6 +8,11 @@ namespace Azure.DataApiBuilder.Config.Converters; +/// +/// This converter is used to convert all the autoentities defined in the configuration file +/// each into a object. The resulting collection is then wrapped in the +/// object. +/// class RuntimeAutoentitiesConverter : JsonConverter { /// diff --git a/src/Config/ObjectModel/AutoentityTemplate.cs b/src/Config/ObjectModel/AutoentityTemplate.cs index 78e7a423a0..2b7e9ad06c 100644 --- a/src/Config/ObjectModel/AutoentityTemplate.cs +++ b/src/Config/ObjectModel/AutoentityTemplate.cs @@ -67,7 +67,7 @@ public AutoentityTemplate( } else { - this.Cache = new EntityCacheOptions(Enabled: true); + this.Cache = new EntityCacheOptions(); } } diff --git a/src/Config/ObjectModel/RuntimeAutoentities.cs b/src/Config/ObjectModel/RuntimeAutoentities.cs index 7879078ee1..0fec45f5a1 100644 --- a/src/Config/ObjectModel/RuntimeAutoentities.cs +++ b/src/Config/ObjectModel/RuntimeAutoentities.cs @@ -13,7 +13,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; public record RuntimeAutoentities { /// - /// The collection of available from the RuntimeConfig. + /// The collection of available from the RuntimeConfig. /// public IReadOnlyDictionary AutoEntities { get; init; } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 5b9e6eb9c1..18d03a7da5 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4289,8 +4289,11 @@ public void FileSinkSerialization( /// /// Test validates that autoentities section can be deserialized and serialized correctly. /// - [TestMethod] + [DataTestMethod] [TestCategory(TestCategory.MSSQL)] + [DataRow(null, null, null, null, null, null, null, null, null)] + [DataRow("%.%", "%.%", "{object}", true, true, true, true, 5, EntityCacheLevel.L1L2)] + [DataRow("%.%", "%.%", "{object}", true, true, true, true, 5, EntityCacheLevel.L1L2)] public void TestAutoEntitiesSerializationDeserialization( string[]? include, string[]? exclude, @@ -4300,9 +4303,7 @@ public void TestAutoEntitiesSerializationDeserialization( bool? healthCheckEnabled, bool? cacheEnabled, int? cacheTTL, - EntityCacheLevel? cacheLevel, - string role, - EntityAction[] entityActions) + EntityCacheLevel? cacheLevel) { TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); @@ -4317,7 +4318,8 @@ public void TestAutoEntitiesSerializationDeserialization( Cache: new EntityCacheOptions(Enabled: cacheEnabled, TtlSeconds: cacheTTL, Level: cacheLevel) ), Permissions: new EntityPermission[1])); - createdAutoentity["test-entity"].Permissions[0] = new EntityPermission(role, entityActions); + + createdAutoentity["test-entity"].Permissions[0] = new EntityPermission("anonymous", new EntityAction[] { new(EntityActionOperation.Read, null, null) }); RuntimeAutoentities autoentities = new(createdAutoentity); FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader(); From 0d977983e3e548184f6f5d436390c011fe6c59ad Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 26 Nov 2025 12:34:53 -0800 Subject: [PATCH 16/25] Changes based on comments --- src/Config/Converters/AutoentityConverter.cs | 6 ++++++ .../Converters/AutoentityPatternsConverter.cs | 8 ++++---- src/Config/ObjectModel/Autoentity.cs | 19 +++---------------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/Config/Converters/AutoentityConverter.cs b/src/Config/Converters/AutoentityConverter.cs index ccd93655c3..591ace6aa8 100644 --- a/src/Config/Converters/AutoentityConverter.cs +++ b/src/Config/Converters/AutoentityConverter.cs @@ -94,6 +94,12 @@ public override void Write(Utf8JsonWriter writer, Autoentity value, JsonSerializ autoentityTemplateConverter.Write(writer, template, options); } + if (value?.UserProvidedPermissionsOptions is true) + { + writer.WritePropertyName("permissions"); + JsonSerializer.Serialize(writer, value.Permissions, options); + } + writer.WriteEndObject(); } } diff --git a/src/Config/Converters/AutoentityPatternsConverter.cs b/src/Config/Converters/AutoentityPatternsConverter.cs index 864ee82abe..d0136227f1 100644 --- a/src/Config/Converters/AutoentityPatternsConverter.cs +++ b/src/Config/Converters/AutoentityPatternsConverter.cs @@ -45,9 +45,9 @@ public AutoentityPatternsConverter(DeserializationVariableReplacementSettings? r { List includeList = new(); - if (reader.TokenType == JsonTokenType.StartObject) + if (reader.TokenType == JsonTokenType.StartArray) { - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { string? value = reader.DeserializeString(_replacementSettings); if (value is not null) @@ -71,9 +71,9 @@ public AutoentityPatternsConverter(DeserializationVariableReplacementSettings? r { List excludeList = new(); - if (reader.TokenType == JsonTokenType.StartObject) + if (reader.TokenType == JsonTokenType.StartArray) { - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { string? value = reader.DeserializeString(_replacementSettings); if (value is not null) diff --git a/src/Config/ObjectModel/Autoentity.cs b/src/Config/ObjectModel/Autoentity.cs index b5455c84ba..3871cd1184 100644 --- a/src/Config/ObjectModel/Autoentity.cs +++ b/src/Config/ObjectModel/Autoentity.cs @@ -24,27 +24,14 @@ public Autoentity( AutoentityTemplate? Template, EntityPermission[]? Permissions) { - if (Patterns is not null) - { - this.Patterns = Patterns; - } - else - { - this.Patterns = new AutoentityPatterns(); - } + this.Patterns = Patterns ?? new AutoentityPatterns(); - if (Template is not null) - { - this.Template = Template; - } - else - { - this.Template = new AutoentityTemplate(); - } + this.Template = Template ?? new AutoentityTemplate(); if (Permissions is not null) { this.Permissions = Permissions; + UserProvidedPermissionsOptions = true; } else { From 8cfde2e256d6d4b7ee5fc2336c60d7dd6028297f Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 26 Nov 2025 14:27:10 -0800 Subject: [PATCH 17/25] Changes based on comments --- .../Configuration/ConfigurationTests.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 18d03a7da5..faee28dbe3 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4291,9 +4291,10 @@ public void FileSinkSerialization( /// [DataTestMethod] [TestCategory(TestCategory.MSSQL)] - [DataRow(null, null, null, null, null, null, null, null, null)] - [DataRow("%.%", "%.%", "{object}", true, true, true, true, 5, EntityCacheLevel.L1L2)] - [DataRow("%.%", "%.%", "{object}", true, true, true, true, 5, EntityCacheLevel.L1L2)] + [DataRow(null, null, null, null, null, null, null, null, null, "anonymous", EntityActionOperation.Read)] + [DataRow("%.%", "%.%", "{object}", true, true, true, false, 5, EntityCacheLevel.L1L2, "anonymous", EntityActionOperation.Read)] + [DataRow("books.%", "books.pages.%", "books_{object}", false, false, false, true, 2147483647, EntityCacheLevel.L1, "test-user", EntityActionOperation.Delete)] + [DataRow(null, "names.%", "{schema}.{object}", true, false, false, true, 1, null, "second-test-user", EntityActionOperation.All)] public void TestAutoEntitiesSerializationDeserialization( string[]? include, string[]? exclude, @@ -4303,7 +4304,9 @@ public void TestAutoEntitiesSerializationDeserialization( bool? healthCheckEnabled, bool? cacheEnabled, int? cacheTTL, - EntityCacheLevel? cacheLevel) + EntityCacheLevel? cacheLevel, + string role, + EntityActionOperation entityActionOp) { TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); @@ -4319,7 +4322,8 @@ public void TestAutoEntitiesSerializationDeserialization( ), Permissions: new EntityPermission[1])); - createdAutoentity["test-entity"].Permissions[0] = new EntityPermission("anonymous", new EntityAction[] { new(EntityActionOperation.Read, null, null) }); + EntityAction[] entityActions = new EntityAction[] { new(entityActionOp, null, null) }; + createdAutoentity["test-entity"].Permissions[0] = new EntityPermission("anonymous", entityActions); RuntimeAutoentities autoentities = new(createdAutoentity); FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader(); @@ -4349,8 +4353,11 @@ public void TestAutoEntitiesSerializationDeserialization( JsonElement root = parsedDocument.RootElement; JsonElement autoentitiesElement = root.GetProperty("autoentities"); + bool entityExists = autoentitiesElement.TryGetProperty("test-entity", out JsonElement entityElement); + Assert.AreEqual(expected: true, actual: entityExists); + // Validate patterns properties and their values exists in autoentities - JsonElement patternsElement = autoentitiesElement.GetProperty("patterns"); + JsonElement patternsElement = entityElement.GetProperty("patterns"); bool includeExists = patternsElement.TryGetProperty("include", out JsonElement includeElement); Assert.AreEqual(expected: true, actual: includeExists); @@ -4368,7 +4375,7 @@ public void TestAutoEntitiesSerializationDeserialization( Assert.AreEqual(expected: name, actual: nameElement.GetString()); // Validate template properties and their values exists in autoentities - JsonElement templateElement = autoentitiesElement.GetProperty("template"); + JsonElement templateElement = entityElement.GetProperty("template"); bool restPropertyExists = templateElement.TryGetProperty("rest", out JsonElement restElement); Assert.AreEqual(expected: (restEnabled != null), actual: restPropertyExists); @@ -4401,7 +4408,7 @@ public void TestAutoEntitiesSerializationDeserialization( Assert.AreEqual(expected: (cacheLevel ?? EntityCacheOptions.DEFAULT_LEVEL).ToString(), actual: cacheLevelElement.GetString()); // Validate permissions properties and their values exists in autoentities - JsonElement permissionsElement = autoentitiesElement.GetProperty("permissions"); + JsonElement permissionsElement = entityElement.GetProperty("permissions"); bool roleExists = permissionsElement.TryGetProperty("role", out JsonElement roleElement); Assert.AreEqual(expected: true, actual: roleExists); From 3a860f145b5dc0f358cc0a25a14949d3c8735cae Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 3 Dec 2025 16:36:14 -0800 Subject: [PATCH 18/25] Fix tests --- src/Config/Converters/AutoentityConverter.cs | 12 +- src/Config/ObjectModel/Autoentity.cs | 26 +--- src/Config/RuntimeConfigLoader.cs | 3 + .../Configuration/ConfigurationTests.cs | 133 +++++++++++------- 4 files changed, 101 insertions(+), 73 deletions(-) diff --git a/src/Config/Converters/AutoentityConverter.cs b/src/Config/Converters/AutoentityConverter.cs index 591ace6aa8..d8cdcce173 100644 --- a/src/Config/Converters/AutoentityConverter.cs +++ b/src/Config/Converters/AutoentityConverter.cs @@ -33,6 +33,11 @@ public AutoentityConverter(DeserializationVariableReplacementSettings? replaceme { if (reader.TokenType == JsonTokenType.EndObject) { + if (permissions == null) + { + throw new JsonException("The 'permissions' property is required for Autoentity."); + } + return new Autoentity(patterns, template, permissions); } @@ -51,6 +56,11 @@ public AutoentityConverter(DeserializationVariableReplacementSettings? replaceme template = templateConverter.Read(ref reader, typeof(AutoentityTemplate), options); break; + case "permissions": + permissions = JsonSerializer.Deserialize(ref reader, options) + ?? throw new JsonException("The 'permissions' property must contain at least one permission."); + break; + default: throw new JsonException($"Unexpected property {propertyName}"); } @@ -94,7 +104,7 @@ public override void Write(Utf8JsonWriter writer, Autoentity value, JsonSerializ autoentityTemplateConverter.Write(writer, template, options); } - if (value?.UserProvidedPermissionsOptions is true) + if (value?.Permissions is not null) { writer.WritePropertyName("permissions"); JsonSerializer.Serialize(writer, value.Permissions, options); diff --git a/src/Config/ObjectModel/Autoentity.cs b/src/Config/ObjectModel/Autoentity.cs index 3871cd1184..913b21ce3e 100644 --- a/src/Config/ObjectModel/Autoentity.cs +++ b/src/Config/ObjectModel/Autoentity.cs @@ -22,34 +22,12 @@ public record Autoentity public Autoentity( AutoentityPatterns? Patterns, AutoentityTemplate? Template, - EntityPermission[]? Permissions) + EntityPermission[] Permissions) { this.Patterns = Patterns ?? new AutoentityPatterns(); this.Template = Template ?? new AutoentityTemplate(); - if (Permissions is not null) - { - this.Permissions = Permissions; - UserProvidedPermissionsOptions = true; - } - else - { - this.Permissions = Array.Empty(); - } + this.Permissions = Permissions; } - - /// - /// Flag which informs CLI and JSON serializer whether to write permissions - /// property and value to the runtime config file. - /// When user doesn't provide the permissions property/value, which signals DAB to use the default, - /// the DAB CLI should not write the default value to a serialized config. - /// This is because the user's intent is to use DAB's default value which could change - /// and DAB CLI writing the property and value would lose the user's intent. - /// This is because if the user were to use the CLI created config, a permissions - /// property/value specified would be interpreted by DAB as "user explicitly set permissions." - /// - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] - [MemberNotNullWhen(true, nameof(Permissions))] - public bool UserProvidedPermissionsOptions { get; init; } = false; } diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index bad5aa8680..3c38766591 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -314,6 +314,9 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AutoentityConverter(replacementSettings)); + options.Converters.Add(new AutoentityPatternsConverter(replacementSettings)); + options.Converters.Add(new AutoentityTemplateConverter(replacementSettings)); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); options.Converters.Add(new MultipleCreateOptionsConverter()); diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index faee28dbe3..8209f9a437 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -40,6 +40,7 @@ using Azure.DataApiBuilder.Service.Tests.SqlTests; using HotChocolate; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -4292,9 +4293,11 @@ public void FileSinkSerialization( [DataTestMethod] [TestCategory(TestCategory.MSSQL)] [DataRow(null, null, null, null, null, null, null, null, null, "anonymous", EntityActionOperation.Read)] - [DataRow("%.%", "%.%", "{object}", true, true, true, false, 5, EntityCacheLevel.L1L2, "anonymous", EntityActionOperation.Read)] - [DataRow("books.%", "books.pages.%", "books_{object}", false, false, false, true, 2147483647, EntityCacheLevel.L1, "test-user", EntityActionOperation.Delete)] - [DataRow(null, "names.%", "{schema}.{object}", true, false, false, true, 1, null, "second-test-user", EntityActionOperation.All)] + [DataRow(new[] { "%.%" }, new[] { "%.%" }, "{object}", true, true, true, false, 5, EntityCacheLevel.L1L2, "anonymous", EntityActionOperation.Read)] + [DataRow(new[] { "books.%" }, new[] { "books.pages.%" }, "books_{object}", false, false, false, true, 2147483647, EntityCacheLevel.L1, "test-user", EntityActionOperation.Delete)] + [DataRow(new[] { "books.%" }, null, "books_{object}", false, null, false, null, 2147483647, null, "test-user", EntityActionOperation.Delete)] + [DataRow(null, new[] { "books.pages.%" }, null, null, false, null, true, null, EntityCacheLevel.L1, "test-user", EntityActionOperation.Delete)] + [DataRow(new[] { "title.%", "books.%", "names.%" }, new[] { "names.%", "%.%" }, "{schema}.{object}", true, false, false, true, 1, null, "second-test-user", EntityActionOperation.Create)] public void TestAutoEntitiesSerializationDeserialization( string[]? include, string[]? exclude, @@ -4317,13 +4320,13 @@ public void TestAutoEntitiesSerializationDeserialization( Template: new AutoentityTemplate( Rest: restEnabled == null ? null : new EntityRestOptions(Enabled: (bool)restEnabled), GraphQL: graphqlEnabled == null ? null : new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: (bool)graphqlEnabled), - Health: new EntityHealthCheckConfig(healthCheckEnabled), - Cache: new EntityCacheOptions(Enabled: cacheEnabled, TtlSeconds: cacheTTL, Level: cacheLevel) + Health: healthCheckEnabled == null ? null : new EntityHealthCheckConfig(healthCheckEnabled), + Cache: (cacheEnabled == null && cacheTTL == null && cacheLevel == null) ? null : new EntityCacheOptions(Enabled: cacheEnabled, TtlSeconds: cacheTTL, Level: cacheLevel) ), Permissions: new EntityPermission[1])); EntityAction[] entityActions = new EntityAction[] { new(entityActionOp, null, null) }; - createdAutoentity["test-entity"].Permissions[0] = new EntityPermission("anonymous", entityActions); + createdAutoentity["test-entity"].Permissions[0] = new EntityPermission(role, entityActions); RuntimeAutoentities autoentities = new(createdAutoentity); FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader(); @@ -4357,66 +4360,100 @@ public void TestAutoEntitiesSerializationDeserialization( Assert.AreEqual(expected: true, actual: entityExists); // Validate patterns properties and their values exists in autoentities - JsonElement patternsElement = entityElement.GetProperty("patterns"); + bool expectedPatternsExist = include != null || exclude != null || name != null; + bool patternsExists = entityElement.TryGetProperty("patterns", out JsonElement patternsElement); + Assert.AreEqual(expected: expectedPatternsExist, actual: patternsExists); - bool includeExists = patternsElement.TryGetProperty("include", out JsonElement includeElement); - Assert.AreEqual(expected: true, actual: includeExists); - Assert.AreEqual(expected: include, actual: includeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); - - bool excludeExists = patternsElement.TryGetProperty("exclude", out JsonElement excludeElement); - Assert.AreEqual(expected: (exclude != null), actual: excludeExists); - if (excludeExists) + if (patternsExists) { - Assert.AreEqual(expected: exclude, actual: excludeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); - } + bool includeExists = patternsElement.TryGetProperty("include", out JsonElement includeElement); + Assert.AreEqual(expected: (include != null), actual: includeExists); + if (includeExists) + { + CollectionAssert.AreEqual(expected: include, actual: includeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); + } + + bool excludeExists = patternsElement.TryGetProperty("exclude", out JsonElement excludeElement); + Assert.AreEqual(expected: (exclude != null), actual: excludeExists); + if (excludeExists) + { + CollectionAssert.AreEqual(expected: exclude, actual: excludeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); + } - bool nameExists = patternsElement.TryGetProperty("name", out JsonElement nameElement); - Assert.AreEqual(expected: true, actual: nameExists); - Assert.AreEqual(expected: name, actual: nameElement.GetString()); + bool nameExists = patternsElement.TryGetProperty("name", out JsonElement nameElement); + Assert.AreEqual(expected: (name != null), actual: nameExists); + if (nameExists) + { + Assert.AreEqual(expected: name, actual: nameElement.GetString()); + } + } // Validate template properties and their values exists in autoentities - JsonElement templateElement = entityElement.GetProperty("template"); + bool expectedTemplateExist = restEnabled != null || graphqlEnabled != null || healthCheckEnabled != null + || cacheEnabled != null || cacheLevel != null || cacheTTL != null; + bool templateExists = entityElement.TryGetProperty("template", out JsonElement templateElement); + Assert.AreEqual(expected: expectedTemplateExist, actual: templateExists); - bool restPropertyExists = templateElement.TryGetProperty("rest", out JsonElement restElement); - Assert.AreEqual(expected: (restEnabled != null), actual: restPropertyExists); - if (restEnabled != null) + if (templateExists) { - Assert.IsTrue(restElement.TryGetProperty("enabled", out JsonElement restEnabledElement)); - Assert.AreEqual(expected: restEnabled, actual: restEnabledElement.GetBoolean()); - } + bool restPropertyExists = templateElement.TryGetProperty("rest", out JsonElement restElement); + Assert.AreEqual(expected: (restEnabled != null), actual: restPropertyExists); + if (restPropertyExists) + { + Assert.IsTrue(restElement.TryGetProperty("enabled", out JsonElement restEnabledElement)); + Assert.AreEqual(expected: restEnabled, actual: restEnabledElement.GetBoolean()); + } - bool graphqlPropertyExists = templateElement.TryGetProperty("graphql", out JsonElement graphqlElement); - Assert.AreEqual(expected: (graphqlEnabled != null), actual: graphqlPropertyExists); - if (graphqlEnabled != null) - { - Assert.IsTrue(graphqlElement.TryGetProperty("enabled", out JsonElement graphqlEnabledElement)); - Assert.AreEqual(expected: graphqlEnabled, actual: graphqlEnabledElement.GetBoolean()); - } + bool graphqlPropertyExists = templateElement.TryGetProperty("graphql", out JsonElement graphqlElement); + Assert.AreEqual(expected: (graphqlEnabled != null), actual: graphqlPropertyExists); + if (graphqlPropertyExists) + { + Assert.IsTrue(graphqlElement.TryGetProperty("enabled", out JsonElement graphqlEnabledElement)); + Assert.AreEqual(expected: graphqlEnabled, actual: graphqlEnabledElement.GetBoolean()); + } + + bool healthPropertyExists = templateElement.TryGetProperty("health", out JsonElement healthElement); + Assert.AreEqual(expected: (healthCheckEnabled != null), actual: healthPropertyExists); + if (healthPropertyExists) + { + Assert.IsTrue(healthElement.TryGetProperty("enabled", out JsonElement healthEnabledElement)); + Assert.AreEqual(expected: healthCheckEnabled, actual: healthEnabledElement.GetBoolean()); + } + + bool expectedCacheExist = cacheEnabled != null || cacheTTL != null || cacheLevel != null; + bool cachePropertyExists = templateElement.TryGetProperty("cache", out JsonElement cacheElement); + Assert.AreEqual(expected: expectedCacheExist, actual: cachePropertyExists); + if (cacheEnabled != null) + { + Assert.IsTrue(cacheElement.TryGetProperty("enabled", out JsonElement cacheEnabledElement)); + Assert.AreEqual(expected: cacheEnabled, actual: cacheEnabledElement.GetBoolean()); + } - bool healthPropertyExists = templateElement.TryGetProperty("health", out JsonElement healthElement); - Assert.AreEqual(expected: true, actual: healthPropertyExists); - Assert.IsTrue(healthElement.TryGetProperty("enabled", out JsonElement healthEnabledElement)); - Assert.AreEqual(expected: (healthCheckEnabled ?? true), actual: healthEnabledElement.GetBoolean()); + if (cacheTTL != null) + { + Assert.IsTrue(cacheElement.TryGetProperty("ttl-seconds", out JsonElement cacheTtlElement)); + Assert.AreEqual(expected: cacheTTL, actual: cacheTtlElement.GetInt32()); + } - bool cachePropertyExists = templateElement.TryGetProperty("cache", out JsonElement cacheElement); - Assert.AreEqual(expected: true, actual: cachePropertyExists); - Assert.IsTrue(cacheElement.TryGetProperty("enabled", out JsonElement cacheEnabledElement)); - Assert.AreEqual(expected: (cacheEnabled ?? false), actual: cacheEnabledElement.GetBoolean()); - Assert.IsTrue(cacheElement.TryGetProperty("ttl", out JsonElement cacheTtlElement)); - Assert.AreEqual(expected: (cacheTTL ?? EntityCacheOptions.DEFAULT_TTL_SECONDS), actual: cacheTtlElement.GetInt32()); - Assert.IsTrue(cacheElement.TryGetProperty("level", out JsonElement cacheLevelElement)); - Assert.AreEqual(expected: (cacheLevel ?? EntityCacheOptions.DEFAULT_LEVEL).ToString(), actual: cacheLevelElement.GetString()); + if (cacheLevel != null) + { + Assert.IsTrue(cacheElement.TryGetProperty("level", out JsonElement cacheLevelElement)); + Assert.IsTrue(string.Equals(cacheLevel.ToString(), cacheLevelElement.GetString(), StringComparison.OrdinalIgnoreCase)); + } + } // Validate permissions properties and their values exists in autoentities JsonElement permissionsElement = entityElement.GetProperty("permissions"); - bool roleExists = permissionsElement.TryGetProperty("role", out JsonElement roleElement); + bool roleExists = permissionsElement[0].TryGetProperty("role", out JsonElement roleElement); Assert.AreEqual(expected: true, actual: roleExists); Assert.AreEqual(expected: role, actual: roleElement.GetString()); - bool entityActionsExists = permissionsElement.TryGetProperty("entity-actions", out JsonElement entityActionsElement); + bool entityActionsExists = permissionsElement[0].TryGetProperty("actions", out JsonElement entityActionsElement); Assert.AreEqual(expected: true, actual: entityActionsExists); - Assert.AreEqual(expected: entityActions.Select(e => e.ToString()).ToArray(), actual: entityActionsElement.EnumerateArray().Select(e => e.ToString()).ToArray()); + bool entityActionOpExists = entityActionsElement[0].TryGetProperty("action", out JsonElement entityActionOpElement); + Assert.AreEqual(expected: true, actual: entityActionOpExists); + Assert.IsTrue(string.Equals(entityActionOp.ToString(), entityActionOpElement.GetString(), StringComparison.OrdinalIgnoreCase)); } } From 014808e6fb182a5abdc0f9a30a25b75da328ea5c Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 3 Dec 2025 16:50:48 -0800 Subject: [PATCH 19/25] Fix failing tests --- src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs index 02b7ca6492..d670b801f8 100644 --- a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs +++ b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs @@ -765,6 +765,7 @@ private static Mock CreateMockRuntimeConfigProvider(strin entities, null, null, + null, null ); mockRuntimeConfig From fcfabef99e9abfa6cabd2ff7445ff131965c1920 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 3 Dec 2025 17:02:53 -0800 Subject: [PATCH 20/25] Fix entity permission --- src/Config/Converters/AutoentityConverter.cs | 7 +------ src/Config/Converters/AutoentityPatternsConverter.cs | 2 +- src/Config/Converters/AutoentityTemplateConverter.cs | 2 +- src/Config/ObjectModel/Autoentity.cs | 4 ++-- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Config/Converters/AutoentityConverter.cs b/src/Config/Converters/AutoentityConverter.cs index d8cdcce173..5c09ed8e7b 100644 --- a/src/Config/Converters/AutoentityConverter.cs +++ b/src/Config/Converters/AutoentityConverter.cs @@ -33,11 +33,6 @@ public AutoentityConverter(DeserializationVariableReplacementSettings? replaceme { if (reader.TokenType == JsonTokenType.EndObject) { - if (permissions == null) - { - throw new JsonException("The 'permissions' property is required for Autoentity."); - } - return new Autoentity(patterns, template, permissions); } @@ -67,7 +62,7 @@ public AutoentityConverter(DeserializationVariableReplacementSettings? replaceme } } - throw new JsonException("Unable to read the Autoentities"); + throw new JsonException("Failed to read the Autoentities"); } /// diff --git a/src/Config/Converters/AutoentityPatternsConverter.cs b/src/Config/Converters/AutoentityPatternsConverter.cs index d0136227f1..d8029ff033 100644 --- a/src/Config/Converters/AutoentityPatternsConverter.cs +++ b/src/Config/Converters/AutoentityPatternsConverter.cs @@ -102,7 +102,7 @@ public AutoentityPatternsConverter(DeserializationVariableReplacementSettings? r } } - throw new JsonException("Unable to read the Autoentities Pattern Options"); + throw new JsonException("Failed to read the Autoentities Pattern Options"); } /// diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs index 78917b6072..6f6540bf9b 100644 --- a/src/Config/Converters/AutoentityTemplateConverter.cs +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -85,7 +85,7 @@ public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? r } } - throw new JsonException("Unable to read the Autoentities Pattern Options"); + throw new JsonException("Failed to read the Autoentities Template Options"); } /// diff --git a/src/Config/ObjectModel/Autoentity.cs b/src/Config/ObjectModel/Autoentity.cs index 913b21ce3e..21d3354fa4 100644 --- a/src/Config/ObjectModel/Autoentity.cs +++ b/src/Config/ObjectModel/Autoentity.cs @@ -22,12 +22,12 @@ public record Autoentity public Autoentity( AutoentityPatterns? Patterns, AutoentityTemplate? Template, - EntityPermission[] Permissions) + EntityPermission[]? Permissions) { this.Patterns = Patterns ?? new AutoentityPatterns(); this.Template = Template ?? new AutoentityTemplate(); - this.Permissions = Permissions; + this.Permissions = Permissions ?? Array.Empty(); } } From e731411bff8ac28a1d3e68f22f17139ece4ecd9d Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 5 Dec 2025 11:07:34 -0800 Subject: [PATCH 21/25] Fix schema --- schemas/dab.draft.schema.json | 45 ++--------------------------------- 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index c5505a476a..710b1fc436 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -740,49 +740,8 @@ "additionalProperties": false, "properties": { "dml-tools": { - "oneOf": [ - { - "type": "boolean", - "description": "Enable/disable all DML tools with default settings." - }, - { - "type": "object", - "description": "Individual DML tools configuration", - "additionalProperties": false, - "properties": { - "describe-entities": { - "type": "boolean", - "description": "Enable/disable the describe-entities tool.", - "default": true - }, - "create-record": { - "type": "boolean", - "description": "Enable/disable the create-record tool.", - "default": true - }, - "read-records": { - "type": "boolean", - "description": "Enable/disable the read-records tool.", - "default": true - }, - "update-record": { - "type": "boolean", - "description": "Enable/disable the update-record tool.", - "default": true - }, - "delete-record": { - "type": "boolean", - "description": "Enable/disable the delete-record tool.", - "default": true - }, - "execute-entity": { - "type": "boolean", - "description": "Enable/disable the execute-entity tool.", - "default": true - } - } - } - ] + "type": "boolean", + "description": "Enable/disable all DML tools with default settings." } } }, From 8fc87af3e5863f5b793922b55857fc307d801bab Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 5 Dec 2025 11:15:49 -0800 Subject: [PATCH 22/25] Fix formatting issues --- src/Config/ObjectModel/Autoentity.cs | 1 - src/Service.Tests/Configuration/ConfigurationTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Config/ObjectModel/Autoentity.cs b/src/Config/ObjectModel/Autoentity.cs index 21d3354fa4..45fca68642 100644 --- a/src/Config/ObjectModel/Autoentity.cs +++ b/src/Config/ObjectModel/Autoentity.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Azure.DataApiBuilder.Config.ObjectModel; diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 8209f9a437..031c93a878 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -40,7 +40,6 @@ using Azure.DataApiBuilder.Service.Tests.SqlTests; using HotChocolate; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; From 1f840bde70b82e10f560cc980a36c154d8211d20 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 18 Dec 2025 18:09:43 -0800 Subject: [PATCH 23/25] Changes based on comments --- schemas/dab.draft.schema.json | 10 ++++------ .../Converters/AutoentityTemplateConverter.cs | 2 +- .../Caching/DabCacheServiceIntegrationTests.cs | 14 +++++++------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 710b1fc436..207eb375ce 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -695,20 +695,18 @@ }, "autoentities": { "type": "object", - "description": "Auto-entity definitions for pattern matching", + "description": "Defines automatic entity generation rules for MSSQL tables based on include/exclude patterns and defaults.", "patternProperties": { "^.*$": { "type": "object", - "additionalProperties": false, "properties": { "patterns": { "type": "object", "description": "Pattern matching rules for including/excluding database objects", - "additionalProperties": false, "properties": { "include": { "type": "array", - "description": "T-SQL LIKE pattern to include database objects (e.g., '%.%')", + "description": "MSSQL LIKE pattern for objects to include (e.g., '%.%'). Null includes all.", "items": { "type": "string" }, @@ -716,7 +714,7 @@ }, "exclude": { "type": "array", - "description": "T-SQL LIKE pattern to exclude database objects (e.g., 'sales.%')", + "description": "MSSQL LIKE pattern for objects to exclude (e.g., 'sales.%'). Null excludes none.", "items": { "type": "string" }, @@ -724,7 +722,7 @@ }, "name": { "type": "string", - "description": "Interpolation syntax for entity naming, must be unique for every entity inside the pattern", + "description": "Entity name interpolation pattern using {schema} and {object}. Null defaults to {object}. Must be unique for every entity inside the pattern", "default": "{object}" } } diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs index 6f6540bf9b..0f922f1898 100644 --- a/src/Config/Converters/AutoentityTemplateConverter.cs +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -68,7 +68,7 @@ public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? r break; case "mcp": - // TODO: Add MCP support for autoentities needed. + mcpd= break; case "health": diff --git a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs index d670b801f8..68c9225b96 100644 --- a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs +++ b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs @@ -760,13 +760,13 @@ private static Mock CreateMockRuntimeConfigProvider(strin }); Mock mockRuntimeConfig = new( - string.Empty, - dataSource, - entities, - null, - null, - null, - null + string.Empty, // Schema + dataSource, // DataSource + entities, // Entities + null, // Autoentities + null, // Runtime + null, // DataSourceFiles + null // AzureKeyVault ); mockRuntimeConfig .Setup(c => c.GetDataSourceFromDataSourceName(It.IsAny())) From 6f8178a1e648c2b787cd5cda5092b54201c271a7 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 18 Dec 2025 18:38:04 -0800 Subject: [PATCH 24/25] Add missing mcp entity property --- .../Converters/AutoentityTemplateConverter.cs | 9 ++++-- src/Config/ObjectModel/AutoentityTemplate.cs | 28 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs index 0f922f1898..275cfc4314 100644 --- a/src/Config/Converters/AutoentityTemplateConverter.cs +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -33,6 +33,10 @@ public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? r JsonConverter graphQLOptionsConverter = (JsonConverter)(graphQLOptionsConverterFactory.CreateConverter(typeof(EntityGraphQLOptions), options) ?? throw new JsonException("Unable to create converter for EntityGraphQLOptions")); + EntityMcpOptionsConverterFactory mcpOptionsConverterFactory = new(); + JsonConverter mcpOptionsConverter = (JsonConverter)(mcpOptionsConverterFactory.CreateConverter(typeof(EntityMcpOptions), options) + ?? throw new JsonException("Unable to create converter for EntityMcpOptions")); + EntityHealthOptionsConvertorFactory healthOptionsConverterFactory = new(); JsonConverter healthOptionsConverter = (JsonConverter)(healthOptionsConverterFactory.CreateConverter(typeof(EntityHealthCheckConfig), options) ?? throw new JsonException("Unable to create converter for EntityHealthCheckConfig")); @@ -44,6 +48,7 @@ public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? r // Initialize all sub-properties to null. EntityRestOptions? rest = null; EntityGraphQLOptions? graphQL = null; + EntityMcpOptions? mcp = null; EntityHealthCheckConfig? health = null; EntityCacheOptions? cache = null; @@ -51,7 +56,7 @@ public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? r { if (reader.TokenType == JsonTokenType.EndObject) { - return new AutoentityTemplate(rest, graphQL, health, cache); + return new AutoentityTemplate(rest, graphQL, mcp, health, cache); } string? propertyName = reader.GetString(); @@ -68,7 +73,7 @@ public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? r break; case "mcp": - mcpd= + mcp = mcpOptionsConverter.Read(ref reader, typeof(EntityMcpOptions), options); break; case "health": diff --git a/src/Config/ObjectModel/AutoentityTemplate.cs b/src/Config/ObjectModel/AutoentityTemplate.cs index 2b7e9ad06c..78a804d0a4 100644 --- a/src/Config/ObjectModel/AutoentityTemplate.cs +++ b/src/Config/ObjectModel/AutoentityTemplate.cs @@ -16,8 +16,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// Cache configuration public record AutoentityTemplate { - // TODO: Will add Mcp variable once MCP is supported at an entity level - // public EntityMcpOptions? Mcp { get; init; } + public EntityMcpOptions? Mcp { get; init; } public EntityRestOptions Rest { get; init; } public EntityGraphQLOptions GraphQL { get; init; } public EntityHealthCheckConfig Health { get; init; } @@ -27,6 +26,7 @@ public record AutoentityTemplate public AutoentityTemplate( EntityRestOptions? Rest = null, EntityGraphQLOptions? GraphQL = null, + EntityMcpOptions? Mcp = null, EntityHealthCheckConfig? Health = null, EntityCacheOptions? Cache = null) { @@ -50,6 +50,16 @@ public AutoentityTemplate( this.GraphQL = new EntityGraphQLOptions(string.Empty, string.Empty); } + if (Mcp is not null) + { + this.Mcp = Mcp; + UserProvidedMcpOptions = true; + } + else + { + this.Mcp = new EntityMcpOptions(null, null); + } + if (Health is not null) { this.Health = Health; @@ -99,6 +109,20 @@ public AutoentityTemplate( [MemberNotNullWhen(true, nameof(GraphQL))] public bool UserProvidedGraphQLOptions { get; init; } = false; + /// + /// Flag which informs CLI and JSON serializer whether to write mcp + /// property and value to the runtime config file. + /// When user doesn't provide the mcp property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a mcp + /// property/value specified would be interpreted by DAB as "user explicitly set mcp." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Mcp))] + public bool UserProvidedMcpOptions { get; init; } = false; + /// /// Flag which informs CLI and JSON serializer whether to write health /// property and value to the runtime config file. From 80ff05541280aca3c9a43fe6b01784e20a447e8f Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 22 Dec 2025 13:18:27 -0800 Subject: [PATCH 25/25] Add additionalProperties to schema --- schemas/dab.draft.schema.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 3885ead633..b6a7f66268 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -699,10 +699,12 @@ "patternProperties": { "^.*$": { "type": "object", + "additionalProperties": false, "properties": { "patterns": { "type": "object", "description": "Pattern matching rules for including/excluding database objects", + "additionalProperties": false, "properties": { "include": { "type": "array",