Skip to content

[Configuration] Initialize optional properties with defaults #1684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 6, 2023
2 changes: 1 addition & 1 deletion schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"mode": {
"description": "Set if running in Development or Production mode",
"type": "string",
"default": "production",
"default": "development",
"enum": [
"production",
"development"
Expand Down
9 changes: 2 additions & 7 deletions src/Cli.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ public static Process ExecuteDabCommand(string command, string flags)
""graphql"": {
""path"": ""/graphql"",
""enabled"": true,
""allow-introspection"": true
""allow-introspection"": false
},
""host"": {
""mode"": ""production"",
Expand Down Expand Up @@ -915,11 +915,6 @@ public static Process ExecuteDabCommand(string command, string flags)
""connection-string"": ""localhost:5000;User ID={USER_NAME};Password={USER_PASSWORD};MultipleActiveResultSets=False;""
},
""runtime"": {
""graphql"": {
""path"": ""/graphql"",
""enabled"": true,
""allow-introspection"": true
},
""host"": {
""mode"": ""production"",
""cors"": {
Expand Down Expand Up @@ -988,7 +983,7 @@ public static Process ExecuteDabCommand(string command, string flags)
""graphql"": {
""path"": ""/graphql"",
""enabled"": true,
""allow-introspection"": true
""allow-introspection"": false
},
""host"": {
""mode"": ""production"",
Expand Down
44 changes: 0 additions & 44 deletions src/Cli/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,50 +204,6 @@ public static bool IsURIComponentValid(string? uriComponent)
return !DoesUriComponentContainReservedChars(uriComponent);
}

/// <summary>
/// Returns the default host Global Settings
/// If the user doesn't specify host mode. Default value to be used is Production.
/// Sample:
// "host": {
// "mode": "production",
// "cors": {
// "origins": [],
// "allow-credentials": true
// },
// "authentication": {
// "provider": "StaticWebApps"
// }
// }
/// </summary>
public static HostOptions GetDefaultHostOptions(
HostMode hostMode,
IEnumerable<string>? corsOrigin,
string authenticationProvider,
string? audience,
string? issuer)
{
string[]? corsOriginArray = corsOrigin is null ? new string[] { } : corsOrigin.ToArray();
CorsOptions cors = new(Origins: corsOriginArray);
AuthenticationOptions AuthenticationOptions;
if (Enum.TryParse<EasyAuthType>(authenticationProvider, ignoreCase: true, out _)
|| AuthenticationOptions.SIMULATOR_AUTHENTICATION.Equals(authenticationProvider))
{
AuthenticationOptions = new(Provider: authenticationProvider, null);
}
else
{
AuthenticationOptions = new(
Provider: authenticationProvider,
Jwt: new(audience, issuer)
);
}

return new(
Mode: hostMode,
Cors: cors,
Authentication: AuthenticationOptions);
}

/// <summary>
/// Returns an object of type Policy
/// If policyRequest or policyDatabase is provided. Otherwise, returns null.
Expand Down
2 changes: 1 addition & 1 deletion src/Config/ObjectModel/AuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// </param>
/// <param name="Jwt">Settings enabling validation of the received JWT token.
/// Required only when Provider is other than EasyAuth.</param>
public record AuthenticationOptions(string Provider, JwtOptions? Jwt)
public record AuthenticationOptions(string Provider = nameof(EasyAuthType.StaticWebApps), JwtOptions? Jwt = null)
{
public const string SIMULATOR_AUTHENTICATION = "Simulator";
public const string CLIENT_PRINCIPAL_HEADER = "X-MS-CLIENT-PRINCIPAL";
Expand Down
16 changes: 15 additions & 1 deletion src/Config/ObjectModel/CorsOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;

/// <summary>
Expand All @@ -9,4 +11,16 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// <param name="Origins">List of allowed origins.</param>
/// <param name="AllowCredentials">
/// Whether to set Access-Control-Allow-Credentials CORS header.</param>
public record CorsOptions(string[] Origins, bool AllowCredentials = false);
public record CorsOptions
{
public string[] Origins;

public bool AllowCredentials;

[JsonConstructor]
public CorsOptions(string[]? Origins, bool AllowCredentials = false)
{
this.Origins = Origins is null ? new string[] { } : Origins.ToArray();
this.AllowCredentials = AllowCredentials;
}
}
47 changes: 46 additions & 1 deletion src/Config/ObjectModel/HostOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,49 @@

namespace Azure.DataApiBuilder.Config.ObjectModel;

public record HostOptions(CorsOptions? Cors, AuthenticationOptions? Authentication, HostMode Mode = HostMode.Development);
public record HostOptions(CorsOptions? Cors, AuthenticationOptions? Authentication, HostMode Mode = HostMode.Development)
{
/// <summary>
/// Returns the default host Global Settings
/// If the user doesn't specify host mode. Default value to be used is Development.
/// Sample:
// "host": {
// "mode": "development",
// "cors": {
// "origins": [],
// "allow-credentials": true
// },
// "authentication": {
// "provider": "StaticWebApps"
// }
// }
/// </summary>
public static HostOptions GetDefaultHostOptions(
HostMode hostMode,
IEnumerable<string>? corsOrigin,
string authenticationProvider,
string? audience,
string? issuer)
{
string[]? corsOriginArray = corsOrigin is null ? new string[] { } : corsOrigin.ToArray();
CorsOptions cors = new(Origins: corsOriginArray);
AuthenticationOptions AuthenticationOptions;
if (Enum.TryParse<EasyAuthType>(authenticationProvider, ignoreCase: true, out _)
|| AuthenticationOptions.SIMULATOR_AUTHENTICATION.Equals(authenticationProvider))
{
AuthenticationOptions = new(Provider: authenticationProvider, Jwt: null);
}
else
{
AuthenticationOptions = new(
Provider: authenticationProvider,
Jwt: new(audience, issuer)
);
}

return new(
Mode: hostMode,
Cors: cors,
Authentication: AuthenticationOptions);
}
}
32 changes: 27 additions & 5 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,34 @@

namespace Azure.DataApiBuilder.Config.ObjectModel;

public record RuntimeConfig(
[property: JsonPropertyName("$schema")] string Schema,
DataSource DataSource,
RuntimeOptions Runtime,
RuntimeEntities Entities)
public record RuntimeConfig
{
[JsonPropertyName("$schema")]
public string Schema;

public const string DEFAULT_CONFIG_SCHEMA_LINK = "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json";

public DataSource DataSource;

public RuntimeOptions Runtime;

public RuntimeEntities Entities;

[JsonConstructor]
public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Entities, RuntimeOptions? Runtime = null)
{
this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK;

this.DataSource = DataSource;
this.Runtime = Runtime ??
new RuntimeOptions(
new RestRuntimeOptions(Enabled: DataSource.DatabaseType != DatabaseType.CosmosDB_NoSQL),
GraphQL: null, // even though we pass null here, the constructor will take care of initializing with defaults.
Host: null);

this.Entities = Entities;
}

/// <summary>
/// Serializes the RuntimeConfig object to JSON for writing to file.
/// </summary>
Expand Down
23 changes: 22 additions & 1 deletion src/Config/ObjectModel/RuntimeOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;

public record RuntimeOptions(RestRuntimeOptions Rest, GraphQLRuntimeOptions GraphQL, HostOptions Host, string? BaseRoute = null);
public record RuntimeOptions
{
public RestRuntimeOptions Rest;
public GraphQLRuntimeOptions GraphQL;
public HostOptions Host;
public string? BaseRoute;

[JsonConstructor]
public RuntimeOptions(RestRuntimeOptions? Rest, GraphQLRuntimeOptions? GraphQL, HostOptions? Host, string? BaseRoute = null)
{
this.Rest = Rest ?? new RestRuntimeOptions();
this.GraphQL = GraphQL ?? new GraphQLRuntimeOptions();
this.Host = Host ?? HostOptions.GetDefaultHostOptions(
HostMode.Development,
corsOrigin: null,
EasyAuthType.StaticWebApps.ToString(),
audience: null, issuer: null);
this.BaseRoute = BaseRoute;
}
}
29 changes: 2 additions & 27 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,32 +64,6 @@ public static bool TryParseConfig(string json,
{
config = config with { DataSource = config.DataSource with { ConnectionString = connectionString } };
}

// For Cosmos DB NoSQL database type, DAB CLI v0.8.49+ generates a REST property within the Runtime section of the config file. However
// v0.7.6- does not generate this property. So, when the config file generated using v0.7.6- is used to start the engine with v0.8.49+, the absence
// of the REST property causes the engine to throw exceptions. This is the only difference in the way Runtime section of the config file is created
// between these two versions.
// To avoid the NullReference Exceptions, the REST property is added when absent in the config file.
// Other properties within the Runtime section are also populated with default values to account for the cases where
// the properties could be removed manually from the config file.
if (config.Runtime is not null)
{
if (config.Runtime.Rest is null)
{
config = config with { Runtime = config.Runtime with { Rest = (config.DataSource.DatabaseType is DatabaseType.CosmosDB_NoSQL) ? new RestRuntimeOptions(Enabled: false) : new RestRuntimeOptions(Enabled: false) } };
}

if (config.Runtime.GraphQL is null)
{
config = config with { Runtime = config.Runtime with { GraphQL = new GraphQLRuntimeOptions(AllowIntrospection: false) } };
}

if (config.Runtime.Host is null)
{
config = config with { Runtime = config.Runtime with { Host = new HostOptions(Cors: null, Authentication: new AuthenticationOptions(Provider: EasyAuthType.StaticWebApps.ToString(), Jwt: null), Mode: HostMode.Production) } };
}
}

}
catch (JsonException ex)
{
Expand Down Expand Up @@ -127,7 +101,8 @@ public static JsonSerializerOptions GetSerializationOptions(bool replaceEnvVar =
PropertyNamingPolicy = new HyphenatedNamingPolicy(),
ReadCommentHandling = JsonCommentHandling.Skip,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
IncludeFields = true
};
options.Converters.Add(new EnumMemberJsonEnumConverterFactory());
options.Converters.Add(new RestRuntimeOptionsConverterFactory());
Expand Down
3 changes: 2 additions & 1 deletion src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1994,7 +1994,8 @@ private static JsonContent GetPostStartupConfigParams(string environment, Runtim
}
else if (configurationEndpoint == CONFIGURATION_ENDPOINT_V2)
{
RuntimeConfig overrides = new(null, new DataSource(DatabaseType.MSSQL, connectionString, new()), null, null);
RuntimeConfig overrides = new(null, new DataSource(DatabaseType.MSSQL, connectionString, new()),
Entities: null, runtimeConfig.Runtime);

ConfigurationPostParametersV2 returnParams = new(
Configuration: serializedConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Data;
using System.IO;
using System.IO.Abstractions.TestingHelpers;
using System.Text;
using System.Text.Json;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.Converters;
Expand All @@ -15,8 +16,7 @@
namespace Azure.DataApiBuilder.Service.Tests.UnitTests
{
/// <summary>
/// Unit tests for the environment variable
/// parser for the runtime configuration. These
/// Unit tests for deserializing the runtime configuration. These
/// tests verify that we parse the config correctly
/// when replacing environment variables. Also verify
/// we throw the right exception when environment
Expand Down Expand Up @@ -114,6 +114,34 @@ public void CheckCommentParsingInConfigFile()
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(actualJson, out RuntimeConfig _), "Should not fail to parse with comments");
}

/// <summary>
/// Method to validate that comments are skipped in config file (and are ignored during deserialization).
/// </summary>
[TestMethod]
public void TestDefaultsCreatedForOptionalProps()
{
// Test with no runtime property
StringBuilder minJson = new(@"
""data-source"": {
""database-type"": ""mssql"",
""connection-string"": ""@env('test-connection-string')""
},
""entities"": { }");
TryParseAndAssertOnDefaults("{" + minJson + "}");

// Test with an empty runtime property
minJson.Append(@", ""runtime"": ");
TryParseAndAssertOnDefaults("{" + minJson + "{ }}");

// Test with empty rest, graphql, host properties
minJson.Append(@"{ ""rest"": { }, ""graphql"": { }, ""host"" : ");
TryParseAndAssertOnDefaults("{" + minJson + "{ } } }", isHostSpecifiedButEmpty: true);

// Test with empty rest, graphql, and empty host sub-properties
minJson.Append(@"{ ""cors"": { }, ""authentication"": { } } }");
TryParseAndAssertOnDefaults("{" + minJson + "}", isHostSpecifiedButEmpty: false);
}

#endregion Positive Tests

#region Negative Tests
Expand Down Expand Up @@ -288,6 +316,31 @@ public static string GetModifiedJsonString(string[] reps, string enumString)
";
}

private static void TryParseAndAssertOnDefaults(string json, bool isHostSpecifiedButEmpty = false)
{
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(json, out RuntimeConfig parsedConfig));
Assert.AreEqual(RuntimeConfig.DEFAULT_CONFIG_SCHEMA_LINK, parsedConfig.Schema);
Assert.IsTrue(parsedConfig.Runtime.Rest.Enabled);
Assert.AreEqual(RestRuntimeOptions.DEFAULT_PATH, parsedConfig.Runtime.Rest.Path);
Assert.IsTrue(parsedConfig.Runtime.GraphQL.Enabled);
Assert.AreEqual(GraphQLRuntimeOptions.DEFAULT_PATH, parsedConfig.Runtime.GraphQL.Path);
Assert.IsTrue(parsedConfig.Runtime.GraphQL.AllowIntrospection);
Assert.IsNull(parsedConfig.Runtime.BaseRoute);
Assert.AreEqual(HostMode.Development, parsedConfig.Runtime.Host.Mode);
if (isHostSpecifiedButEmpty)
{
Assert.IsNull(parsedConfig.Runtime.Host.Cors);
Assert.IsNull(parsedConfig.Runtime.Host.Authentication);
}
else
{
Assert.AreEqual(0, parsedConfig.Runtime.Host.Cors.Origins.Length);
Assert.IsFalse(parsedConfig.Runtime.Host.Cors.AllowCredentials);
Assert.AreEqual(EasyAuthType.StaticWebApps.ToString(), parsedConfig.Runtime.Host.Authentication.Provider);
Assert.IsNull(parsedConfig.Runtime.Host.Authentication.Jwt);
}
}

#endregion Helper Functions

record StubJsonType(string Foo);
Expand Down