Skip to content

Commit 9bbe252

Browse files
Add schema and serialization support for autoentities wildcard enhancement (#2973)
## Why make this change? We need to add the new 'autoentities' properties that will later be used to allow the user to add multiple entities at the same time. The properties need to have all the necessary components to be serialized and deserialized from the config file. ## What is this change? - This change adds the `autoentities` property to the schema file with proper structure using `additionalProperties` to allow user-defined autoentity definition names. - Created new files that allow for the properties to be serialized and deserialized: - `AutoentityConverter.cs` - `AutoentityPatternsConverter.cs` - `AutoentityTemplateConverter.cs` - `RuntimeAutoentitiesConverter.cs` - Created new files where deserialized properties are turned to usable objects: - `Autoentity.cs` - `AutoentityPatterns.cs` - `AutoentityTemplate.cs` - `RuntimeAutoentities.cs` - Added entity-level MCP configuration support: - `EntityMcpOptions.cs` - Supports both boolean shorthand and object format for MCP configuration - `EntityMcpOptionsConverterFactory.cs` - Handles serialization/deserialization of MCP options - Registered autoentity converters in `RuntimeConfigLoader.cs` for proper deserialization. - Updated schema description to "Defines automatic entity generation rules for MSSQL tables based on include/exclude patterns and defaults." - Added `required: ["permissions"]` constraint to enforce at least one permission per specification. **Schema Structure**: The autoentities object uses `additionalProperties` to allow any string key as a user-defined autoentity definition name (e.g., "public-tables", "admin-tables", etc.), consistent with how the "entities" section works. **MCP Configuration**: Supports two formats: - Boolean shorthand: `"mcp": true` or `"mcp": false` - Object format: `"mcp": { "dml-tools": true}` Example configuration: ```json { "autoentities": { "<user-defined name>": { // This property name is decided by the user to show a group of autoentities "patterns": { "include": ["dbo.%"], "exclude": ["dbo.internal_%"], "name": "{object}" }, "template": { "mcp": { "dml-tools": true }, "rest": { "enabled": true }, "graphql": { "enabled": true }, "health": { "enabled": true }, "cache": { "enabled": false } }, "permissions": [ { "role": "anonymous", "actions": ["read"] } ] } } } ``` ## How was this tested? - [ ] Integration Tests - [x] Unit Tests --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: RubenCerna2079 <[email protected]> Co-authored-by: Ruben Cerna <[email protected]>
1 parent 8d62beb commit 9bbe252

15 files changed

+1092
-7
lines changed

schemas/dab.draft.schema.json

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,166 @@
693693
}
694694
}
695695
},
696+
"autoentities": {
697+
"type": "object",
698+
"description": "Defines automatic entity generation rules for MSSQL tables based on include/exclude patterns and defaults.",
699+
"patternProperties": {
700+
"^.*$": {
701+
"type": "object",
702+
"additionalProperties": false,
703+
"properties": {
704+
"patterns": {
705+
"type": "object",
706+
"description": "Pattern matching rules for including/excluding database objects",
707+
"additionalProperties": false,
708+
"properties": {
709+
"include": {
710+
"type": "array",
711+
"description": "MSSQL LIKE pattern for objects to include (e.g., '%.%'). Null includes all.",
712+
"items": {
713+
"type": "string"
714+
},
715+
"default": [ "%.%" ]
716+
},
717+
"exclude": {
718+
"type": "array",
719+
"description": "MSSQL LIKE pattern for objects to exclude (e.g., 'sales.%'). Null excludes none.",
720+
"items": {
721+
"type": "string"
722+
},
723+
"default": null
724+
},
725+
"name": {
726+
"type": "string",
727+
"description": "Entity name interpolation pattern using {schema} and {object}. Null defaults to {object}. Must be unique for every entity inside the pattern",
728+
"default": "{object}"
729+
}
730+
}
731+
},
732+
"template": {
733+
"type": "object",
734+
"description": "Template configuration for generated entities",
735+
"additionalProperties": false,
736+
"properties": {
737+
"mcp": {
738+
"type": "object",
739+
"description": "MCP endpoint configuration",
740+
"additionalProperties": false,
741+
"properties": {
742+
"dml-tools": {
743+
"type": "boolean",
744+
"description": "Enable/disable all DML tools with default settings."
745+
}
746+
}
747+
},
748+
"rest": {
749+
"type": "object",
750+
"description": "REST endpoint configuration",
751+
"additionalProperties": false,
752+
"properties": {
753+
"enabled": {
754+
"type": "boolean",
755+
"description": "Enable/disable REST endpoint",
756+
"default": true
757+
}
758+
}
759+
},
760+
"graphql": {
761+
"type": "object",
762+
"description": "GraphQL endpoint configuration",
763+
"additionalProperties": false,
764+
"properties": {
765+
"enabled": {
766+
"type": "boolean",
767+
"description": "Enable/disable GraphQL endpoint",
768+
"default": true
769+
}
770+
}
771+
},
772+
"health": {
773+
"type": "object",
774+
"description": "Health check configuration",
775+
"additionalProperties": false,
776+
"properties": {
777+
"enabled": {
778+
"type": "boolean",
779+
"description": "Enable/disable health check endpoint",
780+
"default": true
781+
}
782+
}
783+
},
784+
"cache": {
785+
"type": "object",
786+
"description": "Cache configuration",
787+
"additionalProperties": false,
788+
"properties": {
789+
"enabled": {
790+
"type": "boolean",
791+
"description": "Enable/disable caching",
792+
"default": false
793+
},
794+
"ttl-seconds": {
795+
"type": [ "integer", "null" ],
796+
"description": "Time-to-live for cached responses in seconds",
797+
"default": null,
798+
"minimum": 1
799+
},
800+
"level": {
801+
"type": "string",
802+
"description": "Cache level (L1 or L1L2)",
803+
"enum": [ "L1", "L1L2", null ],
804+
"default": "L1L2"
805+
}
806+
}
807+
}
808+
}
809+
},
810+
"permissions": {
811+
"type": "array",
812+
"description": "Permissions assigned to this object",
813+
"items": {
814+
"type": "object",
815+
"additionalProperties": false,
816+
"properties": {
817+
"role": {
818+
"type": "string"
819+
},
820+
"actions": {
821+
"oneOf": [
822+
{
823+
"type": "string",
824+
"pattern": "[*]"
825+
},
826+
{
827+
"type": "array",
828+
"items": {
829+
"oneOf": [
830+
{
831+
"$ref": "#/$defs/action"
832+
},
833+
{
834+
"type": "object",
835+
"additionalProperties": false,
836+
"properties": {
837+
"action": {
838+
"$ref": "#/$defs/action"
839+
}
840+
}
841+
}
842+
]
843+
},
844+
"uniqueItems": true
845+
}
846+
]
847+
}
848+
}
849+
},
850+
"required": [ "role", "actions" ]
851+
}
852+
}
853+
}
854+
}
855+
},
696856
"entities": {
697857
"type": "object",
698858
"description": "Entities that will be exposed via REST, GraphQL and/or MCP",
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using Azure.DataApiBuilder.Config.ObjectModel;
7+
8+
namespace Azure.DataApiBuilder.Config.Converters;
9+
10+
internal class AutoentityConverter : JsonConverter<Autoentity>
11+
{
12+
// Settings for variable replacement during deserialization.
13+
private readonly DeserializationVariableReplacementSettings? _replacementSettings;
14+
15+
/// <param name="replacementSettings">Settings for variable replacement during deserialization.
16+
/// If null, no variable replacement will be performed.</param>
17+
public AutoentityConverter(DeserializationVariableReplacementSettings? replacementSettings = null)
18+
{
19+
_replacementSettings = replacementSettings;
20+
}
21+
22+
/// <inheritdoc/>
23+
public override Autoentity? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
24+
{
25+
if (reader.TokenType is JsonTokenType.StartObject)
26+
{
27+
// Initialize all sub-properties to null.
28+
AutoentityPatterns? patterns = null;
29+
AutoentityTemplate? template = null;
30+
EntityPermission[]? permissions = null;
31+
32+
while (reader.Read())
33+
{
34+
if (reader.TokenType == JsonTokenType.EndObject)
35+
{
36+
return new Autoentity(patterns, template, permissions);
37+
}
38+
39+
string? propertyName = reader.GetString();
40+
41+
reader.Read();
42+
switch (propertyName)
43+
{
44+
case "patterns":
45+
AutoentityPatternsConverter patternsConverter = new(_replacementSettings);
46+
patterns = patternsConverter.Read(ref reader, typeof(AutoentityPatterns), options);
47+
break;
48+
49+
case "template":
50+
AutoentityTemplateConverter templateConverter = new(_replacementSettings);
51+
template = templateConverter.Read(ref reader, typeof(AutoentityTemplate), options);
52+
break;
53+
54+
case "permissions":
55+
permissions = JsonSerializer.Deserialize<EntityPermission[]>(ref reader, options)
56+
?? throw new JsonException("The 'permissions' property must contain at least one permission.");
57+
break;
58+
59+
default:
60+
throw new JsonException($"Unexpected property {propertyName}");
61+
}
62+
}
63+
}
64+
65+
throw new JsonException("Failed to read the Autoentities");
66+
}
67+
68+
/// <summary>
69+
/// When writing the autoentities back to a JSON file, only write the properties
70+
/// if they are user provided. This avoids polluting the written JSON file with properties
71+
/// the user most likely omitted when writing the original DAB runtime config file.
72+
/// This Write operation is only used when a RuntimeConfig object is serialized to JSON.
73+
/// </summary>
74+
/// <inheritdoc/>
75+
public override void Write(Utf8JsonWriter writer, Autoentity value, JsonSerializerOptions options)
76+
{
77+
writer.WriteStartObject();
78+
79+
AutoentityPatterns? patterns = value?.Patterns;
80+
if (patterns?.UserProvidedIncludeOptions is true
81+
|| patterns?.UserProvidedExcludeOptions is true
82+
|| patterns?.UserProvidedNameOptions is true)
83+
{
84+
AutoentityPatternsConverter autoentityPatternsConverter = options.GetConverter(typeof(AutoentityPatterns)) as AutoentityPatternsConverter ??
85+
throw new JsonException("Failed to get autoentities.patterns options converter");
86+
writer.WritePropertyName("patterns");
87+
autoentityPatternsConverter.Write(writer, patterns, options);
88+
}
89+
90+
AutoentityTemplate? template = value?.Template;
91+
if (template?.UserProvidedRestOptions is true
92+
|| template?.UserProvidedGraphQLOptions is true
93+
|| template?.UserProvidedHealthOptions is true
94+
|| template?.UserProvidedCacheOptions is true)
95+
{
96+
AutoentityTemplateConverter autoentityTemplateConverter = options.GetConverter(typeof(AutoentityTemplate)) as AutoentityTemplateConverter ??
97+
throw new JsonException("Failed to get autoentities.template options converter");
98+
writer.WritePropertyName("template");
99+
autoentityTemplateConverter.Write(writer, template, options);
100+
}
101+
102+
if (value?.Permissions is not null)
103+
{
104+
writer.WritePropertyName("permissions");
105+
JsonSerializer.Serialize(writer, value.Permissions, options);
106+
}
107+
108+
writer.WriteEndObject();
109+
}
110+
}

0 commit comments

Comments
 (0)