Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,11 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime
DataSource: dataSource,
Runtime: new(
Rest: new(restEnabled, restPath ?? RestRuntimeOptions.DEFAULT_PATH, options.RestRequestBodyStrict is CliBool.True ? true : false),
GraphQL: new(Enabled: graphQLEnabled, Path: graphQLPath, MultipleMutationOptions: multipleMutationOptions),
GraphQL: new(
Enabled: graphQLEnabled,
Path: graphQLPath,
AllowIntrospection: true,
MultipleMutationOptions: multipleMutationOptions),
Mcp: new(
Enabled: mcpEnabled,
Path: mcpPath ?? McpRuntimeOptions.DEFAULT_PATH,
Expand Down
25 changes: 21 additions & 4 deletions src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ internal GraphQLRuntimeOptionsConverter(DeserializationVariableReplacementSettin

public override GraphQLRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.Null)
if (reader.TokenType == JsonTokenType.True)
{
return new GraphQLRuntimeOptions(Enabled: true);
}

if (reader.TokenType == JsonTokenType.Null)
{
return new GraphQLRuntimeOptions();
}
Expand Down Expand Up @@ -181,9 +186,21 @@ internal GraphQLRuntimeOptionsConverter(DeserializationVariableReplacementSettin
public override void Write(Utf8JsonWriter writer, GraphQLRuntimeOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteBoolean("enabled", value.Enabled);
writer.WriteString("path", value.Path);
writer.WriteBoolean("allow-introspection", value.AllowIntrospection);

if (value.Enabled is not null)
{
writer.WriteBoolean("enabled", value.Enabled.Value);
}

if (value.Path is not null)
{
writer.WriteString("path", value.Path);
}

if (value.AllowIntrospection is not null)
{
writer.WriteBoolean("allow-introspection", value.AllowIntrospection.Value);
}

if (value.UserProvidedDepthLimit)
{
Expand Down
26 changes: 22 additions & 4 deletions src/Config/Converters/RestRuntimeOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ private class RestRuntimeOptionsConverter : JsonConverter<RestRuntimeOptions>
{
public override RestRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.Null)
if (reader.TokenType == JsonTokenType.True)
{
return new RestRuntimeOptions(Enabled: true);
}

if (reader.TokenType == JsonTokenType.Null)
{
return new RestRuntimeOptions();
}
Expand All @@ -45,9 +50,22 @@ private class RestRuntimeOptionsConverter : JsonConverter<RestRuntimeOptions>
public override void Write(Utf8JsonWriter writer, RestRuntimeOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteBoolean("enabled", value.Enabled);
writer.WriteString("path", value.Path);
writer.WriteBoolean("request-body-strict", value.RequestBodyStrict);

if (value.Enabled is not null)
{
writer.WriteBoolean("enabled", value.Enabled.Value);
}

if (value.Path is not null)
{
writer.WriteString("path", value.Path);
}

if (value.RequestBodyStrict is not null)
{
writer.WriteBoolean("request-body-strict", value.RequestBodyStrict.Value);
}

writer.WriteEndObject();
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Config/ObjectModel/GraphQLRuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

namespace Azure.DataApiBuilder.Config.ObjectModel;

public record GraphQLRuntimeOptions(bool Enabled = true,
string Path = GraphQLRuntimeOptions.DEFAULT_PATH,
bool AllowIntrospection = true,
public record GraphQLRuntimeOptions(bool? Enabled = null,
string? Path = null,
bool? AllowIntrospection = null,
int? DepthLimit = null,
MultipleMutationOptions? MultipleMutationOptions = null,
bool EnableAggregation = true,
Expand Down
4 changes: 2 additions & 2 deletions src/Config/ObjectModel/RestRuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// for all entities will be exposed.</param>
/// <param name="RequestBodyStrict">When true, extraneous/unmapped fields in the REST request body are rejected.
/// When false, extraneous fields are allowed and ignored.
/// The record default (true) preserves backward compatibility for existing configs that omit this property.
/// The effective runtime default (true) preserves backward compatibility for existing configs that omit this property.
/// When dab init generates a new config, request-body-strict is set to false to allow extraneous fields by default.</param>
public record RestRuntimeOptions(bool Enabled = true, string Path = RestRuntimeOptions.DEFAULT_PATH, bool RequestBodyStrict = true)
public record RestRuntimeOptions(bool? Enabled = null, string? Path = null, bool? RequestBodyStrict = null)
{
public const string DEFAULT_PATH = "/api";
};
23 changes: 8 additions & 15 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,27 +81,22 @@ Runtime is not null &&
/// </summary>
[JsonIgnore]
public bool IsRequestBodyStrict =>
Runtime is null ||
Runtime.Rest is null ||
Runtime.Rest.RequestBodyStrict;
Runtime?.Rest?.RequestBodyStrict ?? true;

/// <summary>
/// Retrieves the value of runtime.graphql.enabled property if present, default is true.
/// </summary>
[JsonIgnore]
public bool IsGraphQLEnabled => Runtime is null ||
Runtime.GraphQL is null ||
Runtime.GraphQL.Enabled;
public bool IsGraphQLEnabled =>
Runtime?.GraphQL?.Enabled ?? true;

/// <summary>
/// Retrieves the value of runtime.rest.enabled property if present, default is true if its not cosmosdb.
/// </summary>
[JsonIgnore]
public bool IsRestEnabled =>
(Runtime is null ||
Runtime.Rest is null ||
Runtime.Rest.Enabled) &&
DataSource?.DatabaseType != DatabaseType.CosmosDB_NoSQL;
(Runtime?.Rest?.Enabled ?? true) &&
DataSource?.DatabaseType != DatabaseType.CosmosDB_NoSQL;

/// <summary>
/// Retrieves the value of runtime.mcp.enabled property if present, default is true.
Expand Down Expand Up @@ -161,7 +156,7 @@ public string RestPath
}
else
{
return Runtime.Rest.Path;
return Runtime.Rest.Path ?? RestRuntimeOptions.DEFAULT_PATH;
}
}
}
Expand All @@ -180,7 +175,7 @@ public string GraphQLPath
}
else
{
return Runtime.GraphQL.Path;
return Runtime.GraphQL.Path ?? GraphQLRuntimeOptions.DEFAULT_PATH;
}
}
}
Expand Down Expand Up @@ -212,9 +207,7 @@ public bool AllowIntrospection
{
get
{
return Runtime is null ||
Runtime.GraphQL is null ||
Runtime.GraphQL.AllowIntrospection;
return Runtime?.GraphQL?.AllowIntrospection ?? true;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,8 @@ public async Task HotReloadValidationFail()
Assert.IsNotNull(lkgRuntimeConfig);

// Capture properties to verify config hasn't changed
bool originalRestEnabled = lkgRuntimeConfig.Runtime.Rest.Enabled;
bool originalGraphQLEnabled = lkgRuntimeConfig.Runtime.GraphQL.Enabled;
bool originalRestEnabled = lkgRuntimeConfig.IsRestEnabled;
bool originalGraphQLEnabled = lkgRuntimeConfig.IsGraphQLEnabled;
bool originalMcpEnabled = lkgRuntimeConfig.Runtime.Mcp.Enabled;

// Act
Expand All @@ -935,9 +935,9 @@ await WaitForConditionAsync(

// Assert - Verify the configuration hasn't changed by comparing properties
Assert.IsNotNull(newRuntimeConfig, "RuntimeConfig should not be null after failed hot-reload.");
Assert.AreEqual(originalRestEnabled, newRuntimeConfig.Runtime.Rest.Enabled,
Assert.AreEqual(originalRestEnabled, newRuntimeConfig.IsRestEnabled,
"REST enabled setting should remain unchanged after hot-reload failure.");
Assert.AreEqual(originalGraphQLEnabled, newRuntimeConfig.Runtime.GraphQL.Enabled,
Assert.AreEqual(originalGraphQLEnabled, newRuntimeConfig.IsGraphQLEnabled,
"GraphQL enabled setting should remain unchanged after hot-reload failure.");
Assert.AreEqual(originalMcpEnabled, newRuntimeConfig.Runtime.Mcp.Enabled,
"MCP enabled setting should remain unchanged after hot-reload failure.");
Expand All @@ -963,8 +963,8 @@ public async Task HotReloadParsingFail()
Assert.IsNotNull(lkgRuntimeConfig);

// Capture properties to verify config hasn't changed
bool originalRestEnabled = lkgRuntimeConfig.Runtime.Rest.Enabled;
bool originalGraphQLEnabled = lkgRuntimeConfig.Runtime.GraphQL.Enabled;
bool originalRestEnabled = lkgRuntimeConfig.IsRestEnabled;
bool originalGraphQLEnabled = lkgRuntimeConfig.IsGraphQLEnabled;

// Act
GenerateConfigFile(
Expand All @@ -982,9 +982,9 @@ await WaitForConditionAsync(

// Assert - Verify the configuration hasn't changed by comparing properties
Assert.IsNotNull(newRuntimeConfig, "RuntimeConfig should not be null after failed hot-reload.");
Assert.AreEqual(originalRestEnabled, newRuntimeConfig.Runtime.Rest.Enabled,
Assert.AreEqual(originalRestEnabled, newRuntimeConfig.IsRestEnabled,
"REST enabled setting should remain unchanged after hot-reload failure.");
Assert.AreEqual(originalGraphQLEnabled, newRuntimeConfig.Runtime.GraphQL.Enabled,
Assert.AreEqual(originalGraphQLEnabled, newRuntimeConfig.IsGraphQLEnabled,
"GraphQL enabled setting should remain unchanged after hot-reload failure.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,108 @@ public void TestNullableOptionalProps()
TryParseAndAssertOnDefaults("{" + emptyTelemetrySubProps, out _);
}

/// <summary>
/// Verifies that optional scalar runtime properties which are omitted from
/// the input configuration remain omitted after serialization, while their
/// effective runtime behavior still uses the documented defaults.
/// </summary>
[TestMethod]
public void TestOptionalScalarPropsRemainOmittedAfterSerialization()
{
string json = @"{
""data-source"": {
""database-type"": ""mssql"",
""connection-string"": ""@env('test-connection-string')""
},
""runtime"": {
""rest"": { },
""graphql"": { }
},
""entities"": { }
}";

Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(json, out RuntimeConfig runtimeConfig));

string serialized = runtimeConfig.ToJson();
using JsonDocument document = JsonDocument.Parse(serialized);

JsonElement runtime = document.RootElement.GetProperty("runtime");

JsonElement rest = runtime.GetProperty("rest");
Assert.IsFalse(rest.TryGetProperty("enabled", out _));
Assert.IsFalse(rest.TryGetProperty("path", out _));
Assert.IsFalse(rest.TryGetProperty("request-body-strict", out _));

JsonElement graphql = runtime.GetProperty("graphql");
Assert.IsFalse(graphql.TryGetProperty("enabled", out _));
Assert.IsFalse(graphql.TryGetProperty("path", out _));
Assert.IsFalse(graphql.TryGetProperty("allow-introspection", out _));

Assert.IsTrue(runtimeConfig.IsRestEnabled);
Assert.AreEqual(RestRuntimeOptions.DEFAULT_PATH, runtimeConfig.RestPath);
Assert.IsTrue(runtimeConfig.IsRequestBodyStrict);

Assert.IsTrue(runtimeConfig.IsGraphQLEnabled);
Assert.AreEqual(GraphQLRuntimeOptions.DEFAULT_PATH, runtimeConfig.GraphQLPath);
Assert.IsTrue(runtimeConfig.AllowIntrospection);
}

/// <summary>
/// Verifies that omitted optional scalar properties do not materialize
/// defaults during serialization and overwrite values from a base config.
/// </summary>
[TestMethod]
public void TestOptionalScalarPropsDoNotOverrideBaseValuesAfterSerialization()
{
string baseJson = @"{
""runtime"": {
""rest"": {
""enabled"": false,
""path"": ""/base-rest"",
""request-body-strict"": false
},
""graphql"": {
""enabled"": false,
""path"": ""/base-graphql"",
""allow-introspection"": false
}
}
}";

string overrideJson = @"{
""data-source"": {
""database-type"": ""mssql"",
""connection-string"": ""@env('test-connection-string')""
},
""runtime"": {
""rest"": { },
""graphql"": { }
},
""entities"": { }
}";

Assert.IsTrue(
RuntimeConfigLoader.TryParseConfig(
overrideJson,
out RuntimeConfig overrideConfig));

string serializedOverride = overrideConfig.ToJson();
string mergedJson = MergeJsonProvider.Merge(baseJson, serializedOverride);

using JsonDocument document = JsonDocument.Parse(mergedJson);
JsonElement runtime = document.RootElement.GetProperty("runtime");

JsonElement rest = runtime.GetProperty("rest");
Assert.IsFalse(rest.GetProperty("enabled").GetBoolean());
Assert.AreEqual("/base-rest", rest.GetProperty("path").GetString());
Assert.IsFalse(rest.GetProperty("request-body-strict").GetBoolean());

JsonElement graphql = runtime.GetProperty("graphql");
Assert.IsFalse(graphql.GetProperty("enabled").GetBoolean());
Assert.AreEqual("/base-graphql", graphql.GetProperty("path").GetString());
Assert.IsFalse(graphql.GetProperty("allow-introspection").GetBoolean());
}

#endregion Positive Tests

#region Negative Tests
Expand Down