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",