diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json
index 6fecc5e5c7..523fe2d189 100644
--- a/schemas/dab.draft.schema.json
+++ b/schemas/dab.draft.schema.json
@@ -161,7 +161,7 @@
"mode": {
"description": "Set if running in Development or Production mode",
"type": "string",
- "default": "production",
+ "default": "development",
"enum": [
"production",
"development"
diff --git a/src/Cli.Tests/ConfigGeneratorTests.cs b/src/Cli.Tests/ConfigGeneratorTests.cs
index 4e7746af37..505c2fc9c8 100644
--- a/src/Cli.Tests/ConfigGeneratorTests.cs
+++ b/src/Cli.Tests/ConfigGeneratorTests.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.Text;
+
namespace Cli.Tests;
///
@@ -119,6 +121,77 @@ public void TryGenerateConfig_UsingEnvironmentVariable(
}
}
+ ///
+ /// Test to verify creation of initial config with special characters
+ /// such as [!,@,#,$,%,^,&,*, ,(,)] in connection-string or graphql api
+ ///
+ [TestMethod]
+ public void TestSpecialCharactersInConnectionString()
+ {
+ HandleConfigFileCreationAndDeletion(TEST_RUNTIME_CONFIG_FILE, configFilePresent: false);
+ InitOptions options = new(
+ databaseType: DatabaseType.MSSQL,
+ connectionString: "A!string@with#some$special%characters^to&check*proper(serialization).'",
+ cosmosNoSqlDatabase: null,
+ cosmosNoSqlContainer: null,
+ graphQLSchemaPath: null,
+ graphQLPath: "/An_",
+ setSessionContext: false,
+ hostMode: HostMode.Production,
+ corsOrigin: null,
+ authenticationProvider: EasyAuthType.StaticWebApps.ToString(),
+ config: TEST_RUNTIME_CONFIG_FILE);
+
+ StringBuilder expectedRuntimeConfigJson = new(
+ @"{" +
+ @"""$schema"": """ + DAB_DRAFT_SCHEMA_TEST_PATH + @"""" + "," +
+ @"""data-source"": {
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""A!string@with#some$special%characters^to&check*proper(serialization).'"",
+ ""options"":{
+ ""set-session-context"": false
+ }
+ },
+ ""runtime"": {
+ ""rest"": {
+ ""enabled"": true,
+ ""path"": ""/api""
+ },
+ ""graphql"": {
+ ""enabled"": true,
+ ""path"": ""/An_"",
+ ""allow-introspection"": true
+ },
+ ""host"": {
+ ""cors"": {
+ ""origins"": [],
+ ""allow-credentials"": false
+ },
+ ""authentication"": {
+ ""provider"": ""StaticWebApps""
+ },
+ ""mode"": ""production""
+ }
+ },
+ ""entities"": {}
+ }");
+
+ expectedRuntimeConfigJson = expectedRuntimeConfigJson.Replace(" ", string.Empty);
+ expectedRuntimeConfigJson = expectedRuntimeConfigJson.Replace("\n", string.Empty);
+ expectedRuntimeConfigJson = expectedRuntimeConfigJson.Replace("\r\n", string.Empty);
+
+ Assert.IsTrue(TryGenerateConfig(options, _runtimeConfigLoader!, _fileSystem!));
+
+ StringBuilder actualRuntimeConfigJson = new(_fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE, Encoding.Default));
+ actualRuntimeConfigJson = actualRuntimeConfigJson.Replace(" ", string.Empty);
+ actualRuntimeConfigJson = actualRuntimeConfigJson.Replace("\r\n", string.Empty);
+ actualRuntimeConfigJson = actualRuntimeConfigJson.Replace("\n", string.Empty);
+
+ // Comparing explicit strings here since parsing these into JSON would lose
+ // the test scenario of verifying escaped chars are not written to the file system.
+ Assert.AreEqual(expectedRuntimeConfigJson.ToString(), actualRuntimeConfigJson.ToString());
+ }
+
///
/// This method handles the creation and deletion of a configuration file.
///
diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs
index ee52d67506..5bb4eaedef 100644
--- a/src/Cli.Tests/EndToEndTests.cs
+++ b/src/Cli.Tests/EndToEndTests.cs
@@ -33,6 +33,8 @@ public void TestInitialize()
_cliLogger = loggerFactory.CreateLogger();
SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger());
SetCliUtilsLogger(loggerFactory.CreateLogger());
+
+ Environment.SetEnvironmentVariable($"connection-string", TEST_CONNECTION_STRING);
}
[TestCleanup]
@@ -50,7 +52,7 @@ public void TestCleanup()
public Task TestInitForCosmosDBNoSql()
{
string[] args = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "cosmosdb_nosql",
- "--connection-string", "localhost:5000", "--cosmosdb_nosql-database",
+ "--connection-string", TEST_ENV_CONN_STRING, "--cosmosdb_nosql-database",
"graphqldb", "--cosmosdb_nosql-container", "planet", "--graphql-schema", TEST_SCHEMA_FILE, "--cors-origin", "localhost:3000,www.nolocalhost.com:80" };
Program.Execute(args, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);
@@ -107,7 +109,10 @@ public void TestInitializingRestAndGraphQLGlobalSettings()
string[] args = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "mssql", "--rest.path", "/rest-api", "--rest.disabled", "--graphql.path", "/graphql-api" };
Program.Execute(args, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);
- Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));
+ Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(
+ TEST_RUNTIME_CONFIG_FILE,
+ out RuntimeConfig? runtimeConfig,
+ replaceEnvVar: true));
Assert.IsNotNull(runtimeConfig);
Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType);
@@ -124,7 +129,8 @@ public void TestInitializingRestAndGraphQLGlobalSettings()
[TestMethod]
public void TestAddEntity()
{
- string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--host-mode", "development", "--database-type", "mssql", "--connection-string", "localhost:5000", "--auth.provider", "StaticWebApps" };
+ string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--host-mode", "development", "--database-type",
+ "mssql", "--connection-string", TEST_ENV_CONN_STRING, "--auth.provider", "StaticWebApps" };
Program.Execute(initArgs, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));
@@ -140,6 +146,7 @@ public void TestAddEntity()
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? addRuntimeConfig));
Assert.IsNotNull(addRuntimeConfig);
+ Assert.AreEqual(TEST_ENV_CONN_STRING, addRuntimeConfig.DataSource.ConnectionString);
Assert.AreEqual(1, addRuntimeConfig.Entities.Count()); // 1 new entity added
Assert.IsTrue(addRuntimeConfig.Entities.ContainsKey("todo"));
Entity entity = addRuntimeConfig.Entities["todo"];
@@ -374,7 +381,7 @@ public Task TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType()
public void TestUpdateEntity()
{
string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type",
- "mssql", "--connection-string", "localhost:5000" };
+ "mssql", "--connection-string", TEST_ENV_CONN_STRING };
Program.Execute(initArgs, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));
@@ -416,6 +423,7 @@ public void TestUpdateEntity()
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? updateRuntimeConfig));
Assert.IsNotNull(updateRuntimeConfig);
+ Assert.AreEqual(TEST_ENV_CONN_STRING, updateRuntimeConfig.DataSource.ConnectionString);
Assert.AreEqual(2, updateRuntimeConfig.Entities.Count()); // No new entity added
Assert.IsTrue(updateRuntimeConfig.Entities.ContainsKey("todo"));
diff --git a/src/Cli.Tests/EnvironmentTests.cs b/src/Cli.Tests/EnvironmentTests.cs
index 40a5b48e69..ba89eaebcc 100644
--- a/src/Cli.Tests/EnvironmentTests.cs
+++ b/src/Cli.Tests/EnvironmentTests.cs
@@ -123,7 +123,8 @@ public void TestSystemEnvironmentVariableIsUsedInAbsenceOfEnvironmentFile()
[TestMethod]
public void TestStartWithEnvFileIsSuccessful()
{
- BootstrapTestEnvironment("CONN_STRING=test_connection_string");
+ string expectedEnvVarName = "CONN_STRING";
+ BootstrapTestEnvironment(expectedEnvVarName + "=test_connection_string", expectedEnvVarName);
// Trying to start the runtime engine
using Process process = ExecuteDabCommand(
@@ -148,10 +149,11 @@ public void TestStartWithEnvFileIsSuccessful()
/// I feel confident that the overarching scenario is covered through other testing
/// so disabling temporarily while we investigate should be acceptable.
///
- [TestMethod, Ignore]
+ [TestMethod]
public async Task FailureToStartEngineWhenEnvVarNamedWrong()
{
- BootstrapTestEnvironment("COMM_STRINX=test_connection_string");
+ string expectedEnvVarName = "WRONG_CONN_STRING";
+ BootstrapTestEnvironment("COMM_STRINX=test_connection_string", expectedEnvVarName);
// Trying to start the runtime engine
using Process process = ExecuteDabCommand(
@@ -160,11 +162,12 @@ public async Task FailureToStartEngineWhenEnvVarNamedWrong()
);
string? output = await process.StandardError.ReadLineAsync();
- StringAssert.Contains(output, "Environmental Variable, CONN_STRING, not found.", StringComparison.Ordinal);
+ StringAssert.Contains(output, "Environmental Variable, "
+ + expectedEnvVarName + ", not found.", StringComparison.Ordinal);
process.Kill();
}
- private static void BootstrapTestEnvironment(string envFileContents)
+ private static void BootstrapTestEnvironment(string envFileContents, string connStringEnvName)
{
// Creating environment variable file
File.Create(".env").Close();
@@ -174,7 +177,8 @@ private static void BootstrapTestEnvironment(string envFileContents)
File.Delete(TEST_RUNTIME_CONFIG_FILE);
}
- string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "mssql", "--connection-string", "@env('CONN_STRING')" };
+ string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "mssql",
+ "--connection-string", "@env('" + connStringEnvName + "')" };
Program.Main(initArgs);
}
diff --git a/src/Cli.Tests/InitTests.cs b/src/Cli.Tests/InitTests.cs
index 6d807ae48b..9d6f10bc08 100644
--- a/src/Cli.Tests/InitTests.cs
+++ b/src/Cli.Tests/InitTests.cs
@@ -223,28 +223,6 @@ public void EnsureFailureWhenBothRestAndGraphQLAreDisabled(
Assert.AreEqual(expectedResult, TryCreateRuntimeConfig(options, _runtimeConfigLoader!, _fileSystem!, out RuntimeConfig? _));
}
- ///
- /// Test to verify creation of initial config with special characters
- /// such as [!,@,#,$,%,^,&,*, ,(,)] in connection-string.
- ///
- [TestMethod]
- public Task TestSpecialCharactersInConnectionString()
- {
- InitOptions options = new(
- databaseType: DatabaseType.MSSQL,
- connectionString: "A!string@with#some$special%characters^to&check*proper(serialization)including space.",
- cosmosNoSqlDatabase: null,
- cosmosNoSqlContainer: null,
- graphQLSchemaPath: null,
- setSessionContext: false,
- hostMode: HostMode.Production,
- corsOrigin: null,
- authenticationProvider: EasyAuthType.StaticWebApps.ToString(),
- config: TEST_RUNTIME_CONFIG_FILE);
-
- return ExecuteVerifyTest(options);
- }
-
///
/// Test to verify that an error is thrown when user tries to
/// initialize a config with a file name that already exists.
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt
index 5e1214b293..9e25a5a87b 100644
--- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt
@@ -34,13 +34,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
@@ -70,13 +63,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt
index 448354e7bf..5d6e80254b 100644
--- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt
@@ -34,13 +34,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt
index 02e8c59810..fe18bd13e6 100644
--- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt
@@ -34,13 +34,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt
index 1a1efc0798..823547803d 100644
--- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt
@@ -34,13 +34,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt
index 93d6a27ff2..ace29f3dd2 100644
--- a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt
@@ -34,13 +34,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt
index 550848cedd..4e52f4f6a0 100644
--- a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt
@@ -34,13 +34,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
@@ -70,13 +63,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt
index 9daaed30ba..c4b5e69cdb 100644
--- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt
+++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt
@@ -43,13 +43,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt
index b34d51d71b..0be1585440 100644
--- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt
+++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt
@@ -40,13 +40,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt
index 46c0d7515e..454084ce2f 100644
--- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt
@@ -41,11 +41,7 @@
},
Rest: {
Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
+ Post
],
Enabled: true
},
diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt
index 0c7c948166..cd24d2866d 100644
--- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt
@@ -36,11 +36,7 @@
},
Rest: {
Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
+ Post
],
Enabled: true
},
diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt
index 942242e415..95e19f41ef 100644
--- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt
@@ -38,13 +38,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt
index 030301961d..66ce2669d5 100644
--- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt
@@ -38,13 +38,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs
index 4f362a5d33..207ef89916 100644
--- a/src/Cli.Tests/TestHelper.cs
+++ b/src/Cli.Tests/TestHelper.cs
@@ -8,6 +8,9 @@ public static class TestHelper
// Config file name for tests
public const string TEST_RUNTIME_CONFIG_FILE = "dab-config-test.json";
+ public const string TEST_CONNECTION_STRING = "testconnectionstring";
+ public const string TEST_ENV_CONN_STRING = "@env('connection-string')";
+
// test schema for cosmosDB
public const string TEST_SCHEMA_FILE = "test-schema.gql";
public const string DAB_DRAFT_SCHEMA_TEST_PATH = "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json";
@@ -39,6 +42,7 @@ public static Process ExecuteDabCommand(string command, string flags)
StartInfo =
{
FileName = @"./Microsoft.DataApiBuilder",
+ CreateNoWindow = true,
Arguments = $"{command} {flags}",
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
@@ -186,17 +190,17 @@ public static Process ExecuteDabCommand(string command, string flags)
public const string SINGLE_ENTITY_WITH_ONLY_READ_PERMISSION = @"
{
""entities"": {
- ""MyEntity"": {
+ ""MyEntity"": {
""source"": ""s001.book"",
""permissions"": [
- {
+ {
""role"": ""anonymous"",
""actions"": [
""read""
]
- }
+ }
]
- }
+ }
}
}";
@@ -864,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"",
@@ -911,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"": {
@@ -984,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"",
diff --git a/src/Cli.Tests/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs
index 66ef99d3f3..f63e2a22ab 100644
--- a/src/Cli.Tests/UpdateEntityTests.cs
+++ b/src/Cli.Tests/UpdateEntityTests.cs
@@ -1003,7 +1003,7 @@ public void EnsureFailure_AddRelationshipToEntityWithDisabledGraphQL()
Entity sampleEntity1 = new(
Source: new("SOURCE1", EntitySourceType.Table, null, null),
- Rest: new(Array.Empty()),
+ Rest: new(Enabled: true),
GraphQL: new("SOURCE1", "SOURCE1s"),
Permissions: new[] { permissionForEntity },
Relationships: null,
@@ -1013,7 +1013,7 @@ public void EnsureFailure_AddRelationshipToEntityWithDisabledGraphQL()
// entity with graphQL disabled
Entity sampleEntity2 = new(
Source: new("SOURCE2", EntitySourceType.Table, null, null),
- Rest: new(Array.Empty()),
+ Rest: new(Enabled: true),
GraphQL: new("SOURCE2", "SOURCE2s", false),
Permissions: new[] { permissionForEntity },
Relationships: null,
diff --git a/src/Cli.Tests/UtilsTests.cs b/src/Cli.Tests/UtilsTests.cs
index 550c8eebe6..84c36a54f7 100644
--- a/src/Cli.Tests/UtilsTests.cs
+++ b/src/Cli.Tests/UtilsTests.cs
@@ -18,35 +18,35 @@ public void TestInitialize()
[TestMethod]
public void ConstructRestOptionsForCosmosDbNoSQLIgnoresOtherParamsAndDisables()
{
- EntityRestOptions options = ConstructRestOptions("true", Array.Empty(), true);
+ EntityRestOptions options = ConstructRestOptions(restRoute: "true", supportedHttpVerbs: null, isCosmosDbNoSql: true);
Assert.IsFalse(options.Enabled);
}
[TestMethod]
public void ConstructRestOptionsWithNullEnablesRest()
{
- EntityRestOptions options = ConstructRestOptions(null, Array.Empty(), false);
+ EntityRestOptions options = ConstructRestOptions(restRoute: null, supportedHttpVerbs: null, isCosmosDbNoSql: false);
Assert.IsTrue(options.Enabled);
}
[TestMethod]
public void ConstructRestOptionsWithTrueEnablesRest()
{
- EntityRestOptions options = ConstructRestOptions("true", Array.Empty(), false);
+ EntityRestOptions options = ConstructRestOptions(restRoute: "true", supportedHttpVerbs: null, isCosmosDbNoSql: false);
Assert.IsTrue(options.Enabled);
}
[TestMethod]
public void ConstructRestOptionsWithFalseDisablesRest()
{
- EntityRestOptions options = ConstructRestOptions("false", Array.Empty(), false);
+ EntityRestOptions options = ConstructRestOptions(restRoute: "false", supportedHttpVerbs: null, isCosmosDbNoSql: false);
Assert.IsFalse(options.Enabled);
}
[TestMethod]
public void ConstructRestOptionsWithCustomPathSetsPath()
{
- EntityRestOptions options = ConstructRestOptions("customPath", Array.Empty(), false);
+ EntityRestOptions options = ConstructRestOptions(restRoute: "customPath", supportedHttpVerbs: null, isCosmosDbNoSql: false);
Assert.AreEqual("/customPath", options.Path);
Assert.IsTrue(options.Enabled);
}
@@ -57,6 +57,7 @@ public void ConstructRestOptionsWithCustomPathAndMethodsSetsPathAndMethods()
EntityRestOptions options = ConstructRestOptions("customPath", new[] { SupportedHttpVerb.Get, SupportedHttpVerb.Post }, false);
Assert.AreEqual("/customPath", options.Path);
Assert.IsTrue(options.Enabled);
+ Assert.IsNotNull(options.Methods);
Assert.AreEqual(2, options.Methods.Length);
Assert.IsTrue(options.Methods.Contains(SupportedHttpVerb.Get));
Assert.IsTrue(options.Methods.Contains(SupportedHttpVerb.Post));
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 2f4a16bd40..dc6eb4bf24 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -303,7 +303,7 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt
}
GraphQLOperation? graphQLOperationsForStoredProcedures = null;
- SupportedHttpVerb[] SupportedRestMethods = EntityRestOptions.DEFAULT_SUPPORTED_VERBS;
+ SupportedHttpVerb[]? SupportedRestMethods = null;
if (isStoredProcedure)
{
if (CheckConflictingGraphQLConfigurationForStoredProcedures(options))
@@ -948,7 +948,7 @@ public static bool VerifyCanUpdateRelationship(RuntimeConfig runtimeConfig, stri
/// It will use the config provided by the user, else based on the environment value
/// it will either merge the config if base config and environmentConfig is present
/// else it will choose a single config based on precedence (left to right) of
- /// overrides < environmentConfig < defaultConfig
+ /// overrides > environmentConfig > defaultConfig
/// Also preforms validation to check connection string is not null or empty.
///
public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
@@ -965,14 +965,19 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
return false;
}
- loader.UpdateBaseConfigFileName(runtimeConfigFile);
+ loader.UpdateConfigFilePath(runtimeConfigFile);
// Validates that config file has data and follows the correct json schema
- if (!loader.TryLoadKnownConfig(out RuntimeConfig? deserializedRuntimeConfig))
+ // Replaces all the environment variables while deserializing when starting DAB.
+ if (!loader.TryLoadKnownConfig(out RuntimeConfig? deserializedRuntimeConfig, replaceEnvVar: true))
{
- _logger.LogError("Failed to parse the config file: {configFile}.", runtimeConfigFile);
+ _logger.LogError("Failed to parse the config file: {runtimeConfigFile}.", runtimeConfigFile);
return false;
}
+ else
+ {
+ _logger.LogInformation("Loaded config file: {runtimeConfigFile}", runtimeConfigFile);
+ }
if (string.IsNullOrWhiteSpace(deserializedRuntimeConfig.DataSource.ConnectionString))
{
@@ -1082,7 +1087,7 @@ private static bool TryAddGraphQLOperationForStoredProcedure(EntityOptions optio
private static EntityRestOptions ConstructUpdatedRestDetails(Entity entity, EntityOptions options, bool isCosmosDbNoSql)
{
// Updated REST Route details
- EntityRestOptions restPath = (options.RestRoute is not null) ? ConstructRestOptions(options.RestRoute, Array.Empty(), isCosmosDbNoSql) : entity.Rest;
+ EntityRestOptions restPath = (options.RestRoute is not null) ? ConstructRestOptions(restRoute: options.RestRoute, supportedHttpVerbs: null, isCosmosDbNoSql: isCosmosDbNoSql) : entity.Rest;
// Updated REST Methods info for stored procedures
SupportedHttpVerb[]? SupportedRestMethods;
@@ -1125,7 +1130,7 @@ private static EntityRestOptions ConstructUpdatedRestDetails(Entity entity, Enti
SupportedRestMethods = new SupportedHttpVerb[] { SupportedHttpVerb.Post };
}
- return restPath with { Methods = SupportedRestMethods ?? Array.Empty() };
+ return restPath with { Methods = SupportedRestMethods };
}
///
diff --git a/src/Cli/ConfigMerger.cs b/src/Cli/ConfigMerger.cs
index a765db5d6d..55a9ae6ad2 100644
--- a/src/Cli/ConfigMerger.cs
+++ b/src/Cli/ConfigMerger.cs
@@ -24,7 +24,7 @@ public static bool TryMergeConfigsIfAvailable(IFileSystem fileSystem, FileSystem
string baseConfigFile = FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME;
string environmentBasedConfigFile = loader.GetFileName(environmentValue, considerOverrides: false);
- if (loader.DoesFileExistInCurrentDirectory(baseConfigFile) && !string.IsNullOrEmpty(environmentBasedConfigFile))
+ if (loader.DoesFileExistInDirectory(baseConfigFile) && !string.IsNullOrEmpty(environmentBasedConfigFile))
{
try
{
diff --git a/src/Cli/Exporter.cs b/src/Cli/Exporter.cs
index b43ad46334..d375c3bb72 100644
--- a/src/Cli/Exporter.cs
+++ b/src/Cli/Exporter.cs
@@ -26,7 +26,10 @@ public static void Export(ExportOptions options, ILogger logger, FileSystemRunti
return;
}
- if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig) || runtimeConfig is null)
+ if (!loader.TryLoadConfig(
+ runtimeConfigFile,
+ out RuntimeConfig? runtimeConfig,
+ replaceEnvVar: true) || runtimeConfig is null)
{
logger.LogError("Failed to read the config file: {runtimeConfigFile}.", runtimeConfigFile);
return;
diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs
index 4390468c19..b7044bfd07 100644
--- a/src/Cli/Utils.cs
+++ b/src/Cli/Utils.cs
@@ -3,9 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
-using System.Text.Encodings.Web;
using System.Text.Json;
-using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.Converters;
using Azure.DataApiBuilder.Config.ObjectModel;
@@ -132,30 +130,6 @@ public class LowerCaseNamingPolicy : JsonNamingPolicy
public static string ConvertName(Enum name) => name.ToString().ToLower();
}
- ///
- /// Returns the Serialization option used to convert objects into JSON.
- /// Not escaping any special unicode characters.
- /// Ignoring properties with null values.
- /// Keeping all the keys in lowercase.
- ///
- public static JsonSerializerOptions GetSerializationOptions()
- {
- JsonSerializerOptions? options = new()
- {
- Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
- WriteIndented = true,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- PropertyNamingPolicy = new LowerCaseNamingPolicy(),
- // As of .NET Core 7, JsonDocument and JsonSerializer only support skipping or disallowing
- // of comments; they do not support loading them. If we set JsonCommentHandling.Allow for either,
- // it will throw an exception.
- ReadCommentHandling = JsonCommentHandling.Skip
- };
-
- options.Converters.Add(new JsonStringEnumConverter(namingPolicy: new LowerCaseNamingPolicy()));
- return options;
- }
-
///
/// Returns true on successful parsing of mappings Dictionary from IEnumerable list.
/// Returns false in case the format of the input is not correct.
@@ -204,50 +178,6 @@ public static bool IsURIComponentValid(string? uriComponent)
return !DoesUriComponentContainReservedChars(uriComponent);
}
- ///
- /// 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"
- // }
- // }
- ///
- public static HostOptions GetDefaultHostOptions(
- HostMode hostMode,
- IEnumerable? 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(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);
- }
-
///
/// Returns an object of type Policy
/// If policyRequest or policyDatabase is provided. Otherwise, returns null.
@@ -813,12 +743,12 @@ public static bool CheckConflictingGraphQLConfigurationForStoredProcedures(Entit
/// Supported HTTP verbs for the entity.
/// True when the entity is a CosmosDB NoSQL entity, and if it is true, REST is disabled.
/// Constructed REST options for the entity.
- public static EntityRestOptions ConstructRestOptions(string? restRoute, SupportedHttpVerb[] supportedHttpVerbs, bool isCosmosDbNoSql)
+ public static EntityRestOptions ConstructRestOptions(string? restRoute, SupportedHttpVerb[]? supportedHttpVerbs, bool isCosmosDbNoSql)
{
// REST is not supported for CosmosDB NoSQL, so we'll forcibly disable it.
if (isCosmosDbNoSql)
{
- return new(Array.Empty(), Enabled: false);
+ return new(Enabled: false);
}
EntityRestOptions restOptions = new(supportedHttpVerbs);
diff --git a/src/Config/Converters/EntityGraphQLOptionsConverter.cs b/src/Config/Converters/EntityGraphQLOptionsConverter.cs
deleted file mode 100644
index 29de0a515a..0000000000
--- a/src/Config/Converters/EntityGraphQLOptionsConverter.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-// 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 EntityGraphQLOptionsConverter : JsonConverter
-{
- ///
- public override EntityGraphQLOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- if (reader.TokenType == JsonTokenType.StartObject)
- {
- string singular = string.Empty;
- string plural = string.Empty;
- bool enabled = true;
- GraphQLOperation? operation = null;
-
- while (reader.Read())
- {
- if (reader.TokenType == JsonTokenType.EndObject)
- {
- return new EntityGraphQLOptions(singular, plural, enabled, operation);
- }
-
- string? property = reader.GetString();
- reader.Read();
-
- switch (property)
- {
- case "enabled":
- enabled = reader.GetBoolean();
- break;
- case "type":
- if (reader.TokenType == JsonTokenType.String)
- {
- singular = reader.DeserializeString() ?? string.Empty;
- }
- else if (reader.TokenType == JsonTokenType.StartObject)
- {
- while (reader.Read())
- {
- if (reader.TokenType == JsonTokenType.EndObject)
- {
- break;
- }
-
- if (reader.TokenType == JsonTokenType.PropertyName)
- {
- string? property2 = reader.GetString();
- reader.Read();
- // it's possible that we won't end up setting the value for singular
- // or plural, but this will then be determined from the entity name
- // when the RuntimeEntities constructor is invoked later in the
- // deserialization process.
- switch (property2)
- {
- case "singular":
- singular = reader.DeserializeString() ?? string.Empty;
- break;
- case "plural":
- plural = reader.DeserializeString() ?? string.Empty;
- break;
- }
- }
- }
- }
- else
- {
- throw new JsonException($"The value for the 'type' property must be a string or an object, but was {reader.TokenType}");
- }
-
- break;
-
- case "operation":
- string? op = reader.DeserializeString();
-
- if (op is not null)
- {
- operation = Enum.Parse(op, ignoreCase: true);
- }
-
- break;
- }
- }
- }
-
- if (reader.TokenType == JsonTokenType.True)
- {
- return new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: true);
- }
-
- if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.Null)
- {
- return new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: false);
- }
-
- if (reader.TokenType == JsonTokenType.String)
- {
- string? singular = reader.DeserializeString();
- return new EntityGraphQLOptions(singular ?? string.Empty, string.Empty);
- }
-
- throw new JsonException();
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, EntityGraphQLOptions value, JsonSerializerOptions options)
- {
- writer.WriteStartObject();
- writer.WriteBoolean("enabled", value.Enabled);
-
- if (value.Operation is not null)
- {
- writer.WritePropertyName("operation");
- JsonSerializer.Serialize(writer, value.Operation, options);
- }
- else if (value.Operation is null && options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull)
- {
- writer.WriteNull("operation");
- }
-
- writer.WriteStartObject("type");
- writer.WriteString("singular", value.Singular);
- writer.WriteString("plural", value.Plural);
- writer.WriteEndObject();
-
- writer.WriteEndObject();
- }
-}
diff --git a/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs
new file mode 100644
index 0000000000..576850b1cb
--- /dev/null
+++ b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs
@@ -0,0 +1,170 @@
+// 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 EntityGraphQLOptionsConverterFactory : JsonConverterFactory
+{
+ /// Determines whether to replace environment variable with its
+ /// value or not while deserializing.
+ private bool _replaceEnvVar;
+
+ ///
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeToConvert.IsAssignableTo(typeof(EntityGraphQLOptions));
+ }
+
+ ///
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new EntityGraphQLOptionsConverter(_replaceEnvVar);
+ }
+
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
+ internal EntityGraphQLOptionsConverterFactory(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
+ }
+
+ private class EntityGraphQLOptionsConverter : 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 EntityGraphQLOptionsConverter(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
+ }
+
+ ///
+ public override EntityGraphQLOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType is JsonTokenType.StartObject)
+ {
+ string singular = string.Empty;
+ string plural = string.Empty;
+ bool enabled = true;
+ GraphQLOperation? operation = null;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType is JsonTokenType.EndObject)
+ {
+ return new EntityGraphQLOptions(singular, plural, enabled, operation);
+ }
+
+ string? property = reader.GetString();
+ reader.Read();
+
+ switch (property)
+ {
+ case "enabled":
+ enabled = reader.GetBoolean();
+ break;
+ case "type":
+ if (reader.TokenType is JsonTokenType.String)
+ {
+ singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty;
+ }
+ else if (reader.TokenType is JsonTokenType.StartObject)
+ {
+ while (reader.Read())
+ {
+ if (reader.TokenType is JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ if (reader.TokenType is JsonTokenType.PropertyName)
+ {
+ string? property2 = reader.GetString();
+ reader.Read();
+ // it's possible that we won't end up setting the value for singular
+ // or plural, but this will then be determined from the entity name
+ // when the RuntimeEntities constructor is invoked later in the
+ // deserialization process.
+ switch (property2)
+ {
+ case "singular":
+ singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty;
+ break;
+ case "plural":
+ plural = reader.DeserializeString(_replaceEnvVar) ?? string.Empty;
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ throw new JsonException($"The value for the 'type' property must be a string or an object, but was {reader.TokenType}");
+ }
+
+ break;
+
+ case "operation":
+ string? op = reader.DeserializeString(_replaceEnvVar);
+
+ if (op is not null)
+ {
+ operation = Enum.Parse(op, ignoreCase: true);
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (reader.TokenType is JsonTokenType.True)
+ {
+ return new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: true);
+ }
+
+ if (reader.TokenType is JsonTokenType.False || reader.TokenType is JsonTokenType.Null)
+ {
+ return new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: false);
+ }
+
+ if (reader.TokenType is JsonTokenType.String)
+ {
+ string? singular = reader.DeserializeString(_replaceEnvVar);
+ return new EntityGraphQLOptions(singular ?? string.Empty, string.Empty);
+ }
+
+ throw new JsonException();
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, EntityGraphQLOptions value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ writer.WriteBoolean("enabled", value.Enabled);
+
+ if (value.Operation is not null)
+ {
+ writer.WritePropertyName("operation");
+ JsonSerializer.Serialize(writer, value.Operation, options);
+ }
+ else if (value.Operation is null && options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull)
+ {
+ writer.WriteNull("operation");
+ }
+
+ writer.WriteStartObject("type");
+ writer.WriteString("singular", value.Singular);
+ writer.WriteString("plural", value.Plural);
+ writer.WriteEndObject();
+
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/src/Config/Converters/EntityRestOptionsConverter.cs b/src/Config/Converters/EntityRestOptionsConverter.cs
deleted file mode 100644
index 21affa31e6..0000000000
--- a/src/Config/Converters/EntityRestOptionsConverter.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-// 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 EntityRestOptionsConverter : JsonConverter
-{
- ///
- public override EntityRestOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- if (reader.TokenType == JsonTokenType.StartObject)
- {
- EntityRestOptions restOptions = new(Methods: Array.Empty(), Path: null, Enabled: true);
- while (reader.Read())
- {
- if (reader.TokenType == JsonTokenType.EndObject)
- {
- break;
- }
-
- string? propertyName = reader.GetString();
-
- switch (propertyName)
- {
- case "path":
- {
- reader.Read();
-
- if (reader.TokenType == JsonTokenType.String || reader.TokenType == JsonTokenType.Null)
- {
- restOptions = restOptions with { Path = reader.DeserializeString() };
- break;
- }
-
- throw new JsonException($"The value of {propertyName} must be a string. Found {reader.TokenType}.");
- }
-
- case "methods":
- {
- List methods = new();
- while (reader.Read())
- {
- if (reader.TokenType == JsonTokenType.StartArray)
- {
- continue;
- }
-
- if (reader.TokenType == JsonTokenType.EndArray)
- {
- break;
- }
-
- methods.Add(EnumExtensions.Deserialize(reader.DeserializeString()!));
- }
-
- restOptions = restOptions with { Methods = methods.ToArray() };
- break;
- }
-
- case "enabled":
- {
- reader.Read();
- restOptions = restOptions with { Enabled = reader.GetBoolean() };
- break;
- }
-
- default:
- throw new JsonException($"Unexpected property {propertyName}");
- }
- }
-
- return restOptions;
- }
-
- if (reader.TokenType == JsonTokenType.String)
- {
- return new EntityRestOptions(Array.Empty(), reader.DeserializeString(), true);
- }
-
- if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False)
- {
- bool enabled = reader.GetBoolean();
- return new EntityRestOptions(
- Methods: Array.Empty(),
- Path: null,
- Enabled: enabled);
- }
-
- throw new JsonException();
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, EntityRestOptions value, JsonSerializerOptions options)
- {
- writer.WriteStartObject();
- writer.WriteBoolean("enabled", value.Enabled);
-
- if (value.Path is not null)
- {
- writer.WriteString("path", value.Path);
- }
- else if (value.Path is null && options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull)
- {
- writer.WriteNull("path");
- }
-
- writer.WriteStartArray("methods");
- foreach (SupportedHttpVerb method in value.Methods)
- {
- writer.WriteStringValue(JsonSerializer.SerializeToElement(method, options).GetString());
- }
-
- writer.WriteEndArray();
- writer.WriteEndObject();
- }
-}
diff --git a/src/Config/Converters/EntityRestOptionsConverterFactory.cs b/src/Config/Converters/EntityRestOptionsConverterFactory.cs
new file mode 100644
index 0000000000..60ada15c9f
--- /dev/null
+++ b/src/Config/Converters/EntityRestOptionsConverterFactory.cs
@@ -0,0 +1,162 @@
+// 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 EntityRestOptionsConverterFactory : JsonConverterFactory
+{
+ /// Determines whether to replace environment variable with its
+ /// value or not while deserializing.
+ private bool _replaceEnvVar;
+
+ ///
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeToConvert.IsAssignableTo(typeof(EntityRestOptions));
+ }
+
+ ///
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new EntityRestOptionsConverter(_replaceEnvVar);
+ }
+
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
+ internal EntityRestOptionsConverterFactory(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
+ }
+
+ internal class EntityRestOptionsConverter : 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 EntityRestOptionsConverter(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
+ }
+
+ ///
+ public override EntityRestOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType is JsonTokenType.StartObject)
+ {
+ EntityRestOptions restOptions = new(Methods: Array.Empty(), Path: null, Enabled: true);
+ while (reader.Read())
+ {
+ if (reader.TokenType is JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ string? propertyName = reader.GetString();
+
+ switch (propertyName)
+ {
+ case "path":
+ {
+ reader.Read();
+
+ if (reader.TokenType is JsonTokenType.String || reader.TokenType is JsonTokenType.Null)
+ {
+ restOptions = restOptions with { Path = reader.DeserializeString(_replaceEnvVar) };
+ break;
+ }
+
+ throw new JsonException($"The value of {propertyName} must be a string. Found {reader.TokenType}.");
+ }
+
+ case "methods":
+ {
+ List methods = new();
+ while (reader.Read())
+ {
+ if (reader.TokenType is JsonTokenType.StartArray)
+ {
+ continue;
+ }
+
+ if (reader.TokenType is JsonTokenType.EndArray)
+ {
+ break;
+ }
+
+ methods.Add(EnumExtensions.Deserialize(reader.DeserializeString(replaceEnvVar: true)!));
+ }
+
+ restOptions = restOptions with { Methods = methods.ToArray() };
+ break;
+ }
+
+ case "enabled":
+ {
+ reader.Read();
+ restOptions = restOptions with { Enabled = reader.GetBoolean() };
+ break;
+ }
+
+ default:
+ throw new JsonException($"Unexpected property {propertyName}");
+ }
+ }
+
+ return restOptions;
+ }
+
+ if (reader.TokenType is JsonTokenType.String)
+ {
+ return new EntityRestOptions(Array.Empty(), reader.DeserializeString(_replaceEnvVar), true);
+ }
+
+ if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False)
+ {
+ bool enabled = reader.GetBoolean();
+ return new EntityRestOptions(
+ Methods: Array.Empty(),
+ Path: null,
+ Enabled: enabled);
+ }
+
+ throw new JsonException();
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, EntityRestOptions value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ writer.WriteBoolean("enabled", value.Enabled);
+
+ if (value.Path is not null)
+ {
+ writer.WriteString("path", value.Path);
+ }
+ else if (value.Path is null && options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull)
+ {
+ writer.WriteNull("path");
+ }
+
+ if (value.Methods is not null && value.Methods.Length > 0)
+ {
+
+ writer.WriteStartArray("methods");
+ foreach (SupportedHttpVerb method in value.Methods)
+ {
+ writer.WriteStringValue(JsonSerializer.SerializeToElement(method, options).GetString());
+ }
+
+ writer.WriteEndArray();
+ }
+
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/src/Config/Converters/EntitySourceConverterFactory.cs b/src/Config/Converters/EntitySourceConverterFactory.cs
index 0b503f3356..51af00717d 100644
--- a/src/Config/Converters/EntitySourceConverterFactory.cs
+++ b/src/Config/Converters/EntitySourceConverterFactory.cs
@@ -9,6 +9,10 @@ namespace Azure.DataApiBuilder.Config.Converters;
internal class EntitySourceConverterFactory : JsonConverterFactory
{
+ // Determines whether to replace environment variable with its
+ // value or not while deserializing.
+ private bool _replaceEnvVar;
+
///
public override bool CanConvert(Type typeToConvert)
{
@@ -18,16 +22,34 @@ public override bool CanConvert(Type typeToConvert)
///
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
- return new EntitySourceConverter();
+ return new EntitySourceConverter(_replaceEnvVar);
+ }
+
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
+ internal EntitySourceConverterFactory(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
}
private class EntitySourceConverter : 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 EntitySourceConverter(bool replaceEnvVar)
+ {
+ _replaceEnvVar = replaceEnvVar;
+ }
+
public override EntitySource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
- string? obj = reader.DeserializeString();
+ string? obj = reader.DeserializeString(_replaceEnvVar);
return new EntitySource(obj ?? string.Empty, EntitySourceType.Table, new(), Array.Empty());
}
diff --git a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs
index 9a1936fb14..1d6dd9f7c4 100644
--- a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs
+++ b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs
@@ -76,7 +76,7 @@ public override bool CanConvert(Type typeToConvert)
{
return (JsonConverter?)Activator.CreateInstance(
typeof(JsonStringEnumConverterEx<>).MakeGenericType(typeToConvert)
- );
+ );
}
internal class JsonStringEnumConverterEx : JsonConverter where TEnum : struct, Enum
@@ -113,7 +113,8 @@ public JsonStringEnumConverterEx()
///
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- string? stringValue = reader.DeserializeString();
+ // Always replace env variable in case of Enum otherwise string to enum conversion will fail.
+ string? stringValue = reader.DeserializeString(replaceEnvVar: true);
if (stringValue == null)
{
diff --git a/src/Config/Converters/RuntimeEntitiesConverter.cs b/src/Config/Converters/RuntimeEntitiesConverter.cs
index 532ca30197..c0a32b78e9 100644
--- a/src/Config/Converters/RuntimeEntitiesConverter.cs
+++ b/src/Config/Converters/RuntimeEntitiesConverter.cs
@@ -26,9 +26,8 @@ public override void Write(Utf8JsonWriter writer, RuntimeEntities value, JsonSer
writer.WriteStartObject();
foreach ((string key, Entity entity) in value.Entities)
{
- string json = JsonSerializer.Serialize(entity, options);
writer.WritePropertyName(key);
- writer.WriteRawValue(json);
+ JsonSerializer.Serialize(writer, entity, options);
}
writer.WriteEndObject();
diff --git a/src/Config/Converters/StringJsonConverterFactory.cs b/src/Config/Converters/StringJsonConverterFactory.cs
index 5732742def..b7d49b8d47 100644
--- a/src/Config/Converters/StringJsonConverterFactory.cs
+++ b/src/Config/Converters/StringJsonConverterFactory.cs
@@ -8,6 +8,10 @@
namespace Azure.DataApiBuilder.Config.Converters;
+///
+/// Custom string json converter factory to replace environment variables of the pattern
+/// @env('ENV_NAME') with their value during deserialization.
+///
public class StringJsonConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
@@ -44,8 +48,7 @@ class StringJsonConverter : JsonConverter
if (reader.TokenType == JsonTokenType.String)
{
string? value = reader.GetString();
-
- return Regex.Replace(reader.GetString()!, ENV_PATTERN, new MatchEvaluator(ReplaceMatchWithEnvVariable));
+ return Regex.Replace(value!, ENV_PATTERN, new MatchEvaluator(ReplaceMatchWithEnvVariable));
}
if (reader.TokenType == JsonTokenType.Null)
diff --git a/src/Config/Converters/Utf8JsonReaderExtensions.cs b/src/Config/Converters/Utf8JsonReaderExtensions.cs
index 3a20c5d20f..b6bfc0fd7d 100644
--- a/src/Config/Converters/Utf8JsonReaderExtensions.cs
+++ b/src/Config/Converters/Utf8JsonReaderExtensions.cs
@@ -13,9 +13,11 @@ static internal class Utf8JsonReaderExtensions
/// substitution is applied.
///
/// The reader that we want to pull the string from.
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
/// The result of deserialization.
/// Thrown if the is not String.
- public static string? DeserializeString(this Utf8JsonReader reader)
+ public static string? DeserializeString(this Utf8JsonReader reader, bool replaceEnvVar)
{
if (reader.TokenType is JsonTokenType.Null)
{
@@ -29,7 +31,11 @@ static internal class Utf8JsonReaderExtensions
// Add the StringConverterFactory so that we can do environment variable substitution.
JsonSerializerOptions options = new();
- options.Converters.Add(new StringJsonConverterFactory());
+ if (replaceEnvVar)
+ {
+ options.Converters.Add(new StringJsonConverterFactory());
+ }
+
return JsonSerializer.Deserialize(ref reader, options);
}
}
diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs
index 6a35224337..1ce068d148 100644
--- a/src/Config/FileSystemRuntimeConfigLoader.cs
+++ b/src/Config/FileSystemRuntimeConfigLoader.cs
@@ -25,7 +25,9 @@ namespace Azure.DataApiBuilder.Config;
///
public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader
{
- private string _baseConfigFileName;
+ // This stores either the default config name e.g. dab-config.json
+ // or user provided config file which could be a relative file path, absolute file path or simply the file name assumed to be in current directory.
+ private string _baseConfigFilePath;
private readonly IFileSystem _fileSystem;
@@ -42,13 +44,21 @@ public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader
///
public const string DEFAULT_CONFIG_FILE_NAME = $"{CONFIGFILE_NAME}{CONFIG_EXTENSION}";
- public string ConfigFileName => GetFileNameForEnvironment(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"), false);
+ ///
+ /// Stores the config file actually loaded by the engine.
+ /// It could be the base config file (e.g. dab-config.json), any of its derivatives with
+ /// environment specific suffixes (e.g. dab-config.Development.json) or the user provided
+ /// config file name.
+ /// It could also be the config file provided by the user.
+ ///
+ public string ConfigFilePath { get; internal set; }
- public FileSystemRuntimeConfigLoader(IFileSystem fileSystem, string baseConfigFileName = DEFAULT_CONFIG_FILE_NAME, string? connectionString = null)
+ public FileSystemRuntimeConfigLoader(IFileSystem fileSystem, string baseConfigFilePath = DEFAULT_CONFIG_FILE_NAME, string? connectionString = null)
: base(connectionString)
{
_fileSystem = fileSystem;
- _baseConfigFileName = baseConfigFileName;
+ _baseConfigFilePath = baseConfigFilePath;
+ ConfigFilePath = GetFinalConfigFilePath();
}
///
@@ -56,13 +66,25 @@ public FileSystemRuntimeConfigLoader(IFileSystem fileSystem, string baseConfigFi
///
/// The path to the dab-config.json file.
/// The loaded RuntimeConfig, or null if none was loaded.
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
/// True if the config was loaded, otherwise false.
- public bool TryLoadConfig(string path, [NotNullWhen(true)] out RuntimeConfig? config)
+ public bool TryLoadConfig(
+ string path,
+ [NotNullWhen(true)] out RuntimeConfig? config,
+ bool replaceEnvVar = false)
{
if (_fileSystem.File.Exists(path))
{
+ Console.WriteLine($"Loading config file from {path}.");
string json = _fileSystem.File.ReadAllText(path);
- return TryParseConfig(json, out config, connectionString: _connectionString);
+ return TryParseConfig(json, out config, connectionString: _connectionString, replaceEnvVar: replaceEnvVar);
+ }
+ else
+ {
+ // Unable to use ILogger because this code is invoked before LoggerFactory
+ // is instantiated.
+ Console.WriteLine($"Unable to find config file: {path} does not exist.");
}
config = null;
@@ -73,10 +95,12 @@ public bool TryLoadConfig(string path, [NotNullWhen(true)] out RuntimeConfig? co
/// Tries to load the config file using the filename known to the RuntimeConfigLoader and for the default environment.
///
/// The loaded RuntimeConfig, or null if none was loaded.
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
/// True if the config was loaded, otherwise false.
- public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config)
+ public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false)
{
- return TryLoadConfig(ConfigFileName, out config);
+ return TryLoadConfig(ConfigFilePath, out config, replaceEnvVar);
}
///
@@ -121,6 +145,29 @@ public string GetFileNameForEnvironment(string? aspnetEnvironment, bool consider
return configFileNameWithExtension;
}
+ ///
+ /// This method returns the final config file name that will be used by the runtime engine.
+ ///
+ private string GetFinalConfigFilePath()
+ {
+ if (!string.Equals(_baseConfigFilePath, DEFAULT_CONFIG_FILE_NAME))
+ {
+ // user provided config file is honoured.
+ return _baseConfigFilePath;
+ }
+
+ // ConfigFile not explicitly provided by user, so we need to get the config file name based on environment.
+ string configFilePath = GetFileNameForEnvironment(Environment.GetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME), false);
+
+ // If file for environment is not found, then the baseConfigFile is used as the final configFile for runtime engine.
+ if (string.IsNullOrWhiteSpace(configFilePath))
+ {
+ return _baseConfigFilePath;
+ }
+
+ return configFilePath;
+ }
+
///
/// Generates the config file name and a corresponding overridden file name,
/// With precedence given to overridden file name, returns that name
@@ -132,21 +179,24 @@ public string GetFileNameForEnvironment(string? aspnetEnvironment, bool consider
///
public string GetFileName(string? environmentValue, bool considerOverrides)
{
- string fileNameWithoutExtension = _fileSystem.Path.GetFileNameWithoutExtension(_baseConfigFileName);
- string fileExtension = _fileSystem.Path.GetExtension(_baseConfigFileName);
- string configFileName =
+ // If the baseConfigFilePath contains directory info, we need to ensure that it is not lost. for example: baseConfigFilePath = "config/dab-config.json"
+ // in this case, we need to get the directory name and the file name without extension and then combine them back. Else, we will lose the path
+ // and the file will be searched in the current directory.
+ string filePathWithoutExtension = _fileSystem.Path.Combine(_fileSystem.Path.GetDirectoryName(_baseConfigFilePath) ?? string.Empty, _fileSystem.Path.GetFileNameWithoutExtension(_baseConfigFilePath));
+ string fileExtension = _fileSystem.Path.GetExtension(_baseConfigFilePath);
+ string configFilePath =
!string.IsNullOrEmpty(environmentValue)
- ? $"{fileNameWithoutExtension}.{environmentValue}"
- : $"{fileNameWithoutExtension}";
- string configFileNameWithExtension = $"{configFileName}{fileExtension}";
- string overriddenConfigFileNameWithExtension = GetOverriddenName(configFileName);
+ ? $"{filePathWithoutExtension}.{environmentValue}"
+ : $"{filePathWithoutExtension}";
+ string configFileNameWithExtension = $"{configFilePath}{fileExtension}";
+ string overriddenConfigFileNameWithExtension = GetOverriddenName(configFilePath);
- if (considerOverrides && DoesFileExistInCurrentDirectory(overriddenConfigFileNameWithExtension))
+ if (considerOverrides && DoesFileExistInDirectory(overriddenConfigFileNameWithExtension))
{
return overriddenConfigFileNameWithExtension;
}
- if (DoesFileExistInCurrentDirectory(configFileNameWithExtension))
+ if (DoesFileExistInDirectory(configFileNameWithExtension))
{
return configFileNameWithExtension;
}
@@ -154,9 +204,9 @@ public string GetFileName(string? environmentValue, bool considerOverrides)
return string.Empty;
}
- private static string GetOverriddenName(string fileName)
+ private static string GetOverriddenName(string filePath)
{
- return $"{fileName}.overrides{CONFIG_EXTENSION}";
+ return $"{filePath}.overrides{CONFIG_EXTENSION}";
}
///
@@ -168,24 +218,16 @@ public static string GetEnvironmentFileName(string fileName, string environmentV
return $"{fileName}.{environmentValue}{CONFIG_EXTENSION}";
}
- public bool DoesFileExistInCurrentDirectory(string fileName)
+ ///
+ /// Checks if the file exists in the directory.
+ /// Works for both relative and absolute paths.
+ ///
+ ///
+ /// True if file is found, else false.
+ public bool DoesFileExistInDirectory(string filePath)
{
string currentDir = _fileSystem.Directory.GetCurrentDirectory();
- // Unable to use ILogger because this code is invoked before LoggerFactory
- // is instantiated.
- if (_fileSystem.File.Exists(_fileSystem.Path.Combine(currentDir, fileName)))
- {
- // This config file is logged as being found, but may not actually be used!
- Console.WriteLine($"Found config file: {fileName}.");
- return true;
- }
- else
- {
- // Unable to use ILogger because this code is invoked before LoggerFactory
- // is instantiated.
- Console.WriteLine($"Unable to find config file: {fileName} does not exist.");
- return false;
- }
+ return _fileSystem.File.Exists(_fileSystem.Path.Combine(currentDir, filePath));
}
///
@@ -244,11 +286,13 @@ public static string GetMergedFileNameForEnvironment(string fileName, string env
}
///
- /// Allows the base config file name to be updated. This is commonly done when the CLI is starting up.
+ /// Allows the base config file and the actually loaded config file name(tracked by the property ConfigFileName)
+ /// to be updated. This is commonly done when the CLI is starting up.
///
///
- public void UpdateBaseConfigFileName(string fileName)
+ public void UpdateConfigFilePath(string filePath)
{
- _baseConfigFileName = fileName;
+ _baseConfigFilePath = filePath;
+ ConfigFilePath = filePath;
}
}
diff --git a/src/Config/ObjectModel/AuthenticationOptions.cs b/src/Config/ObjectModel/AuthenticationOptions.cs
index 6ab281a62b..e74aa35767 100644
--- a/src/Config/ObjectModel/AuthenticationOptions.cs
+++ b/src/Config/ObjectModel/AuthenticationOptions.cs
@@ -11,7 +11,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
///
/// Settings enabling validation of the received JWT token.
/// Required only when Provider is other than EasyAuth.
-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";
diff --git a/src/Config/ObjectModel/CorsOptions.cs b/src/Config/ObjectModel/CorsOptions.cs
index 690325cb02..3f1066b665 100644
--- a/src/Config/ObjectModel/CorsOptions.cs
+++ b/src/Config/ObjectModel/CorsOptions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.Text.Json.Serialization;
+
namespace Azure.DataApiBuilder.Config.ObjectModel;
///
@@ -9,4 +11,16 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// List of allowed origins.
///
/// Whether to set Access-Control-Allow-Credentials CORS header.
-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;
+ }
+}
diff --git a/src/Config/ObjectModel/EntityGraphQLOptions.cs b/src/Config/ObjectModel/EntityGraphQLOptions.cs
index 7e2ad0c6b5..0a09c3bf61 100644
--- a/src/Config/ObjectModel/EntityGraphQLOptions.cs
+++ b/src/Config/ObjectModel/EntityGraphQLOptions.cs
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using System.Text.Json.Serialization;
-using Azure.DataApiBuilder.Config.Converters;
-
namespace Azure.DataApiBuilder.Config.ObjectModel;
///
@@ -14,5 +11,4 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// Indicates if GraphQL is enabled for the entity.
/// When the entity maps to a stored procedure, this represents the GraphQL operation to use, otherwise it will be null.
///
-[JsonConverter(typeof(EntityGraphQLOptionsConverter))]
public record EntityGraphQLOptions(string Singular, string Plural, bool Enabled = true, GraphQLOperation? Operation = null);
diff --git a/src/Config/ObjectModel/EntityRestOptions.cs b/src/Config/ObjectModel/EntityRestOptions.cs
index 1e821b9a8c..1e17b83bc1 100644
--- a/src/Config/ObjectModel/EntityRestOptions.cs
+++ b/src/Config/ObjectModel/EntityRestOptions.cs
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using System.Text.Json.Serialization;
-using Azure.DataApiBuilder.Config.Converters;
-
namespace Azure.DataApiBuilder.Config.ObjectModel;
///
@@ -13,10 +10,11 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// at which the REST endpoint for this entity is exposed
/// instead of using the entity-name. Can be a string type.
///
-/// The HTTP verbs that are supported for this entity.
+/// The HTTP verbs that are supported for this entity. Has significance only for stored-procedures.
+/// For tables and views, all the 5 HTTP actions are enabled when REST endpoints are enabled
+/// for the entity. So, this property is insignificant for tables and views.
/// Whether the entity is enabled for REST.
-[JsonConverter(typeof(EntityRestOptionsConverter))]
-public record EntityRestOptions(SupportedHttpVerb[] Methods, string? Path = null, bool Enabled = true)
+public record EntityRestOptions(SupportedHttpVerb[]? Methods = null, string? Path = null, bool Enabled = true)
{
public static readonly SupportedHttpVerb[] DEFAULT_SUPPORTED_VERBS = new[] { SupportedHttpVerb.Get, SupportedHttpVerb.Post, SupportedHttpVerb.Put, SupportedHttpVerb.Patch, SupportedHttpVerb.Delete };
public static readonly SupportedHttpVerb[] DEFAULT_HTTP_VERBS_ENABLED_FOR_SP = new[] { SupportedHttpVerb.Post };
diff --git a/src/Config/ObjectModel/HostOptions.cs b/src/Config/ObjectModel/HostOptions.cs
index bfe6e5e2b8..1ce1f2f23b 100644
--- a/src/Config/ObjectModel/HostOptions.cs
+++ b/src/Config/ObjectModel/HostOptions.cs
@@ -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)
+{
+ ///
+ /// 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"
+ // }
+ // }
+ ///
+ public static HostOptions GetDefaultHostOptions(
+ HostMode hostMode,
+ IEnumerable? 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(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);
+ }
+}
diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index b3fd6e5919..271c124fc3 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -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;
+ }
+
///
/// Serializes the RuntimeConfig object to JSON for writing to file.
///
diff --git a/src/Config/ObjectModel/RuntimeEntities.cs b/src/Config/ObjectModel/RuntimeEntities.cs
index ad26ecee8b..53554efa9d 100644
--- a/src/Config/ObjectModel/RuntimeEntities.cs
+++ b/src/Config/ObjectModel/RuntimeEntities.cs
@@ -128,9 +128,10 @@ private static Entity ProcessRestDefaults(Entity nameCorrectedEntity)
:
nameCorrectedEntity
with
- { Rest = new EntityRestOptions(EntityRestOptions.DEFAULT_SUPPORTED_VERBS) };
+ // Unless explicilty configured through config file, REST endpoints are enabled for entities backed by tables/views.
+ { Rest = new EntityRestOptions(Enabled: true) };
}
- else if (nameCorrectedEntity.Source.Type is EntitySourceType.StoredProcedure && nameCorrectedEntity.Rest.Methods.Length == 0)
+ else if (nameCorrectedEntity.Source.Type is EntitySourceType.StoredProcedure && (nameCorrectedEntity.Rest.Methods is null || nameCorrectedEntity.Rest.Methods.Length == 0))
{
// REST Method field is relevant only for stored procedures. For an entity backed by a table/view, all HTTP verbs are enabled by design
// unless configured otherwise through the config file. An entity backed by a stored procedure also supports all HTTP verbs but only POST is
diff --git a/src/Config/ObjectModel/RuntimeOptions.cs b/src/Config/ObjectModel/RuntimeOptions.cs
index 015f8a7665..1a3400bb41 100644
--- a/src/Config/ObjectModel/RuntimeOptions.cs
+++ b/src/Config/ObjectModel/RuntimeOptions.cs
@@ -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;
+ }
+}
diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs
index e696633d26..70285aaa6b 100644
--- a/src/Config/RuntimeConfigLoader.cs
+++ b/src/Config/RuntimeConfigLoader.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Diagnostics.CodeAnalysis;
+using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.Converters;
@@ -24,8 +25,10 @@ public RuntimeConfigLoader(string? connectionString = null)
/// Returns RuntimeConfig.
///
/// The loaded RuntimeConfig, or null if none was loaded.
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing.
/// True if the config was loaded, otherwise false.
- public abstract bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config);
+ public abstract bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false);
///
/// Returns the link to the published draft schema.
@@ -39,9 +42,15 @@ public RuntimeConfigLoader(string? connectionString = null)
/// JSON that represents the config file.
/// The parsed config, or null if it parsed unsuccessfully.
/// True if the config was parsed, otherwise false.
- public static bool TryParseConfig(string json, [NotNullWhen(true)] out RuntimeConfig? config, ILogger? logger = null, string? connectionString = null)
+ /// Whether to replace environment variable with its
+ /// value or not while deserializing. By default, no replacement happens.
+ public static bool TryParseConfig(string json,
+ [NotNullWhen(true)] out RuntimeConfig? config,
+ ILogger? logger = null,
+ string? connectionString = null,
+ bool replaceEnvVar = false)
{
- JsonSerializerOptions options = GetSerializationOptions();
+ JsonSerializerOptions options = GetSerializationOptions(replaceEnvVar);
try
{
@@ -83,7 +92,9 @@ public static bool TryParseConfig(string json, [NotNullWhen(true)] out RuntimeCo
///
/// Get Serializer options for the config file.
///
- public static JsonSerializerOptions GetSerializationOptions()
+ /// Whether to replace environment variable with value or not while deserializing.
+ /// By default, no replacement happens.
+ public static JsonSerializerOptions GetSerializationOptions(bool replaceEnvVar = false)
{
JsonSerializerOptions options = new()
{
@@ -91,14 +102,23 @@ public static JsonSerializerOptions GetSerializationOptions()
PropertyNamingPolicy = new HyphenatedNamingPolicy(),
ReadCommentHandling = JsonCommentHandling.Skip,
WriteIndented = true,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ IncludeFields = true,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
options.Converters.Add(new EnumMemberJsonEnumConverterFactory());
options.Converters.Add(new RestRuntimeOptionsConverterFactory());
options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory());
- options.Converters.Add(new EntitySourceConverterFactory());
+ options.Converters.Add(new EntitySourceConverterFactory(replaceEnvVar));
+ options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replaceEnvVar));
+ options.Converters.Add(new EntityRestOptionsConverterFactory(replaceEnvVar));
options.Converters.Add(new EntityActionConverterFactory());
- options.Converters.Add(new StringJsonConverterFactory());
+
+ if (replaceEnvVar)
+ {
+ options.Converters.Add(new StringJsonConverterFactory());
+ }
+
return options;
}
}
diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs
index a3e4376840..a2e0b1e602 100644
--- a/src/Core/Authorization/AuthorizationResolver.cs
+++ b/src/Core/Authorization/AuthorizationResolver.cs
@@ -232,7 +232,15 @@ public void SetEntityPermissionMap(RuntimeConfig runtimeConfig)
bool isStoredProcedureEntity = entity.Source.Type is EntitySourceType.StoredProcedure;
if (isStoredProcedureEntity)
{
- SupportedHttpVerb[] methods = entity.Rest.Methods;
+ SupportedHttpVerb[] methods;
+ if (entity.Rest.Methods is not null)
+ {
+ methods = entity.Rest.Methods;
+ }
+ else
+ {
+ methods = (entity.Rest.Enabled) ? new SupportedHttpVerb[] { SupportedHttpVerb.Post } : Array.Empty();
+ }
entityToRoleMap.StoredProcedureHttpVerbs = new(methods);
}
diff --git a/src/Core/Azure.DataApiBuilder.Core.csproj b/src/Core/Azure.DataApiBuilder.Core.csproj
index 3850c5f587..08fa6c9eb5 100644
--- a/src/Core/Azure.DataApiBuilder.Core.csproj
+++ b/src/Core/Azure.DataApiBuilder.Core.csproj
@@ -4,6 +4,7 @@
net6.0
enable
enable
+ true
@@ -32,6 +33,10 @@
+
+ true
+
+
diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs
index cf335ceac9..0299c9ac0f 100644
--- a/src/Core/Configurations/RuntimeConfigProvider.cs
+++ b/src/Core/Configurations/RuntimeConfigProvider.cs
@@ -54,6 +54,7 @@ public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader)
/// is known by the loader.
///
/// The RuntimeConfig instance.
+ /// Dont use this method if environment variable references need to be retained.
/// Thrown when the loader is unable to load an instance of the config from its known location.
public RuntimeConfig GetConfig()
{
@@ -62,7 +63,8 @@ public RuntimeConfig GetConfig()
return _runtimeConfig;
}
- if (ConfigLoader.TryLoadKnownConfig(out RuntimeConfig? config))
+ // While loading the config file, replace all the environment variables with their values.
+ if (ConfigLoader.TryLoadKnownConfig(out RuntimeConfig? config, replaceEnvVar: true))
{
_runtimeConfig = config;
}
@@ -87,7 +89,7 @@ public bool TryGetConfig([NotNullWhen(true)] out RuntimeConfig? runtimeConfig)
{
if (_runtimeConfig is null)
{
- if (ConfigLoader.TryLoadKnownConfig(out RuntimeConfig? config))
+ if (ConfigLoader.TryLoadKnownConfig(out RuntimeConfig? config, replaceEnvVar: true))
{
_runtimeConfig = config;
}
@@ -131,7 +133,8 @@ public async Task Initialize(
if (RuntimeConfigLoader.TryParseConfig(
configuration,
- out RuntimeConfig? runtimeConfig))
+ out RuntimeConfig? runtimeConfig,
+ replaceEnvVar: true))
{
_runtimeConfig = runtimeConfig;
@@ -181,7 +184,7 @@ public async Task Initialize(string jsonConfig, string? graphQLSchema, str
IsLateConfigured = true;
- if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig))
+ if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, replaceEnvVar: true))
{
_runtimeConfig = runtimeConfig.DataSource.DatabaseType switch
{
diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs
index c9d59083ff..97a2835cea 100644
--- a/src/Core/Configurations/RuntimeConfigValidator.cs
+++ b/src/Core/Configurations/RuntimeConfigValidator.cs
@@ -235,7 +235,7 @@ public static void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(Runti
///
///
/// The runtime configuration.
- public static void ValidateEntityConfiguration(RuntimeConfig runtimeConfig)
+ public void ValidateEntityConfiguration(RuntimeConfig runtimeConfig)
{
// Stores the unique rest paths configured for different entities present in the config.
HashSet restPathsForEntities = new();
@@ -256,6 +256,8 @@ public static void ValidateEntityConfiguration(RuntimeConfig runtimeConfig)
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError
);
}
+
+ ValidateRestMethods(entity, entityName);
}
// If GraphQL endpoint is enabled globally and at entity level, then only we perform the validations related to it.
@@ -267,6 +269,21 @@ public static void ValidateEntityConfiguration(RuntimeConfig runtimeConfig)
}
}
+ ///
+ /// Helper method to validate and let users know whether insignificant properties are present in the REST field.
+ /// Currently, it checks for the presence of Methods property when the entity type is table/view and logs a warning.
+ /// Methods property plays a role only in case of stored procedures.
+ ///
+ /// Entity object for which validation is performed
+ /// Name of the entity
+ private void ValidateRestMethods(Entity entity, string entityName)
+ {
+ if (entity.Source.Type is not EntitySourceType.StoredProcedure && entity.Rest.Methods is not null && entity.Rest.Methods.Length > 0)
+ {
+ _logger.LogWarning("Entity {entityName} has rest methods configured but is not a stored procedure. Values configured will be ignored and all 5 HTTP actions will be enabled.", entityName);
+ }
+ }
+
///
/// Helper method to validate that the rest path property for the entity is correctly configured.
/// The rest path should not be null/empty and should not contain any reserved characters.
diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs
index 30e008721e..13c15d9545 100644
--- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs
+++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs
@@ -450,8 +450,17 @@ private Dictionary GetConfiguredRestOperations(string entit
if (dbObject.SourceType == EntitySourceType.StoredProcedure)
{
- Entity entityTest = _runtimeConfig.Entities[entityName];
- List spRestMethods = entityTest.Rest.Methods.ToList();
+ Entity entity = _runtimeConfig.Entities[entityName];
+
+ List? spRestMethods;
+ if (entity.Rest.Methods is not null)
+ {
+ spRestMethods = entity.Rest.Methods.ToList();
+ }
+ else
+ {
+ spRestMethods = new List { SupportedHttpVerb.Post };
+ }
if (spRestMethods is null)
{
@@ -664,20 +673,20 @@ private static bool IsRequestBodyRequired(SourceDefinition sourceDef, bool consi
}
///
- /// Resolves any REST path overrides present for the provided entity in the runtime config.
- /// If no overrides exist, returns the passed in entity name.
+ /// Attempts to resolve the REST path override set for an entity in the runtime config.
+ /// If no override exists, this method returns the passed in entity name.
///
/// Name of the entity.
- /// Returns the REST path name for the provided entity.
+ /// Returns the REST path name for the provided entity with no starting slash: {entityName} or {entityRestPath}.
private string GetEntityRestPath(string entityName)
{
string entityRestPath = entityName;
EntityRestOptions entityRestSettings = _runtimeConfig.Entities[entityName].Rest;
- if (!string.IsNullOrEmpty(entityRestSettings.Path) && entityRestSettings.Path.StartsWith('/'))
+ if (!string.IsNullOrEmpty(entityRestSettings.Path))
{
- // Remove slash from start of rest path.
- entityRestPath = entityRestPath.Substring(1);
+ // Remove slash from start of REST path.
+ entityRestPath = entityRestSettings.Path.TrimStart('/');
}
return entityRestPath;
diff --git a/src/Core/Services/ResolverMiddleware.cs b/src/Core/Services/ResolverMiddleware.cs
index aa3c857807..d653b9e510 100644
--- a/src/Core/Services/ResolverMiddleware.cs
+++ b/src/Core/Services/ResolverMiddleware.cs
@@ -261,6 +261,8 @@ protected static bool IsInnerObject(IMiddlewareContext context)
SINGLE_TYPE => ((FloatValueNode)value).ToSingle(),
FLOAT_TYPE => ((FloatValueNode)value).ToDouble(),
DECIMAL_TYPE => ((FloatValueNode)value).ToDecimal(),
+ // If we reach here, we can be sure that the value will not be null.
+ UUID_TYPE => Guid.TryParse(value.Value!.ToString(), out Guid guidValue) ? guidValue : value.Value,
_ => value.Value
};
}
diff --git a/src/Core/Services/RestService.cs b/src/Core/Services/RestService.cs
index 8ab9655061..98e2d88a68 100644
--- a/src/Core/Services/RestService.cs
+++ b/src/Core/Services/RestService.cs
@@ -334,7 +334,16 @@ private bool TryGetStoredProcedureRESTVerbs(string entityName, [NotNullWhen(true
{
if (runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity))
{
- SupportedHttpVerb[] methods = entity.Rest.Methods;
+ SupportedHttpVerb[] methods;
+ if (entity.Rest.Methods is not null)
+ {
+ methods = entity.Rest.Methods;
+ }
+ else
+ {
+ methods = (entity.Rest.Enabled) ? new SupportedHttpVerb[] { SupportedHttpVerb.Post } : Array.Empty();
+ }
+
httpVerbs = new(methods);
return true;
}
diff --git a/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs b/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs
index 52c1b25ad4..a3ab42b692 100644
--- a/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs
+++ b/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs
@@ -4,10 +4,13 @@
namespace Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes
{
///
- /// Only used to group the supported type names under a class with a relevant name
+ /// Only used to group the supported type names under a class with a relevant name.
+ /// The type names mentioned here are Hotchocolate scalar built in types.
+ /// The corresponding SQL type name may be different for e.g. UUID maps to Guid as the SQL type.
///
public static class SupportedTypes
{
+ public const string UUID_TYPE = "UUID";
public const string BYTE_TYPE = "Byte";
public const string SHORT_TYPE = "Short";
public const string INT_TYPE = "Int";
@@ -20,6 +23,5 @@ public static class SupportedTypes
public const string DATETIME_TYPE = "DateTime";
public const string DATETIME_NONUTC_TYPE = "DateTimeNonUTC";
public const string BYTEARRAY_TYPE = "ByteArray";
- public const string GUID_TYPE = "Guid";
}
}
diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs
index 3b7d10bb1b..eb0e549473 100644
--- a/src/Service.GraphQLBuilder/GraphQLUtils.cs
+++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs
@@ -42,6 +42,7 @@ public static bool IsBuiltInType(ITypeNode typeNode)
HashSet inBuiltTypes = new()
{
"ID",
+ UUID_TYPE,
BYTE_TYPE,
SHORT_TYPE,
INT_TYPE,
@@ -181,10 +182,14 @@ public static bool TryExtractGraphQLFieldModelName(IDirectiveCollection fieldDir
{
if (dir.Name.Value == ModelDirectiveType.DirectiveName)
{
- dir.ToObject();
- modelName = dir.GetArgument(ModelDirectiveType.ModelNameArgument).ToString();
+ ModelDirectiveType modelDirectiveType = dir.ToObject();
+
+ if (modelDirectiveType.Name.HasValue)
+ {
+ modelName = dir.GetArgument(ModelDirectiveType.ModelNameArgument).ToString();
+ return modelName is not null;
+ }
- return modelName is not null;
}
}
diff --git a/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs b/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs
index 69ead89827..5877bb692e 100644
--- a/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs
+++ b/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs
@@ -169,7 +169,7 @@ public static InputObjectTypeDefinitionNode StringInputType() =>
new InputValueDefinitionNode(null, new NameNode("endsWith"), new StringValueNode("Ends With"), new StringType().ToTypeNode(), null, new List()),
new InputValueDefinitionNode(null, new NameNode("neq"), new StringValueNode("Not Equals"), new StringType().ToTypeNode(), null, new List()),
new InputValueDefinitionNode(null, new NameNode("caseInsensitive"), new StringValueNode("Case Insensitive"), new BooleanType().ToTypeNode(), new BooleanValueNode(false), new List()),
- new InputValueDefinitionNode(null, new NameNode("isNull"), new StringValueNode("Not null test"), new BooleanType().ToTypeNode(), null, new List())
+ new InputValueDefinitionNode(null, new NameNode("isNull"), new StringValueNode("Is null test"), new BooleanType().ToTypeNode(), null, new List())
}
);
@@ -197,13 +197,32 @@ public static InputObjectTypeDefinitionNode ByteArrayInputType() =>
new StringValueNode("Input type for adding ByteArray filters"),
new List(),
new List {
- new InputValueDefinitionNode(null, new NameNode("isNull"), new StringValueNode("Not null test"), new BooleanType().ToTypeNode(), null, new List())
+ new InputValueDefinitionNode(null, new NameNode("isNull"), new StringValueNode("Is null test"), new BooleanType().ToTypeNode(), null, new List())
+ }
+ );
+
+ public static InputObjectTypeDefinitionNode UuidInputType() =>
+ new(
+ location: null,
+ new NameNode("UuidFilterInput"),
+ new StringValueNode("Input type for adding Uuid filters"),
+ new List(),
+ new List {
+ new InputValueDefinitionNode(null, new NameNode("eq"), new StringValueNode("Equals"), new UuidType().ToTypeNode(), null, new List()),
+ new InputValueDefinitionNode(null, new NameNode("contains"), new StringValueNode("Contains"), new UuidType().ToTypeNode(), null, new List()),
+ new InputValueDefinitionNode(null, new NameNode("notContains"), new StringValueNode("Not Contains"), new UuidType().ToTypeNode(), null, new List()),
+ new InputValueDefinitionNode(null, new NameNode("startsWith"), new StringValueNode("Starts With"), new UuidType().ToTypeNode(), null, new List()),
+ new InputValueDefinitionNode(null, new NameNode("endsWith"), new StringValueNode("Ends With"), new UuidType().ToTypeNode(), null, new List()),
+ new InputValueDefinitionNode(null, new NameNode("neq"), new StringValueNode("Not Equals"), new UuidType().ToTypeNode(), null, new List()),
+ new InputValueDefinitionNode(null, new NameNode("caseInsensitive"), new StringValueNode("Case Insensitive"), new BooleanType().ToTypeNode(), new BooleanValueNode(false), new List()),
+ new InputValueDefinitionNode(null, new NameNode("isNull"), new StringValueNode("Is null test"), new BooleanType().ToTypeNode(), null, new List())
}
);
public static Dictionary InputTypes = new()
{
{ "ID", IdInputType() },
+ { UUID_TYPE, UuidInputType() },
{ BYTE_TYPE, ByteInputType() },
{ SHORT_TYPE, ShortInputType() },
{ INT_TYPE, IntInputType() },
diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
index 703dc2c975..70954f400a 100644
--- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
+++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
@@ -219,7 +219,7 @@ public static string GetGraphQLTypeFromSystemType(Type type)
return type.Name switch
{
"String" => STRING_TYPE,
- "Guid" => STRING_TYPE,
+ "Guid" => UUID_TYPE,
"Byte" => BYTE_TYPE,
"Int16" => SHORT_TYPE,
"Int32" => INT_TYPE,
@@ -255,7 +255,7 @@ public static IValueNode CreateValueNodeFromDbObjectMetadata(object metadataValu
short value => new ObjectValueNode(new ObjectFieldNode(SHORT_TYPE, new IntValueNode(value))),
int value => new ObjectValueNode(new ObjectFieldNode(INT_TYPE, value)),
long value => new ObjectValueNode(new ObjectFieldNode(LONG_TYPE, new IntValueNode(value))),
- Guid value => new ObjectValueNode(new ObjectFieldNode(GUID_TYPE, value.ToString())),
+ Guid value => new ObjectValueNode(new ObjectFieldNode(UUID_TYPE, new UuidType().ParseValue(value))),
string value => new ObjectValueNode(new ObjectFieldNode(STRING_TYPE, value)),
bool value => new ObjectValueNode(new ObjectFieldNode(BOOLEAN_TYPE, value)),
float value => new ObjectValueNode(new ObjectFieldNode(SINGLE_TYPE, new SingleType().ParseValue(value))),
diff --git a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs
index 49a0371685..2305eab1f4 100644
--- a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs
+++ b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
#nullable enable
-using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -1260,7 +1259,7 @@ private static RuntimeConfig BuildTestRuntimeConfig(EntityPermission[] permissio
{
Entity sampleEntity = new(
Source: new(entityName, EntitySourceType.Table, null, null),
- Rest: new(Array.Empty()),
+ Rest: new(Enabled: true),
GraphQL: new("", ""),
Permissions: permissions,
Relationships: null,
diff --git a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs
index d17a3e1a60..a068e6300a 100644
--- a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs
+++ b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs
@@ -45,6 +45,10 @@ public void ValidateEasyAuthConfig()
new MockFileData(config.ToJson())
);
+ // Since we added the config file to the filesystem above after the config loader was initialized
+ // in TestInitialize, we need to update the ConfigfileName, otherwise it will be an empty string.
+ _runtimeConfigLoader.UpdateConfigFilePath(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME);
+
try
{
_runtimeConfigValidator.ValidateConfig();
@@ -71,6 +75,8 @@ public void ValidateJwtConfigParamsSet()
new MockFileData(config.ToJson())
);
+ _runtimeConfigLoader.UpdateConfigFilePath(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME);
+
try
{
_runtimeConfigValidator.ValidateConfig();
@@ -90,6 +96,8 @@ public void ValidateAuthNSectionNotNecessary()
new MockFileData(config.ToJson())
);
+ _runtimeConfigLoader.UpdateConfigFilePath(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME);
+
try
{
_runtimeConfigValidator.ValidateConfig();
@@ -117,6 +125,8 @@ public void ValidateFailureWithIncompleteJwtConfig()
new MockFileData(config.ToJson())
);
+ _runtimeConfigLoader.UpdateConfigFilePath(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME);
+
Assert.ThrowsException(() =>
{
_runtimeConfigValidator.ValidateConfig();
@@ -150,6 +160,8 @@ public void ValidateFailureWithUnneededEasyAuthConfig()
new MockFileData(config.ToJson())
);
+ _runtimeConfigLoader.UpdateConfigFilePath(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME);
+
Assert.ThrowsException(() =>
{
_runtimeConfigValidator.ValidateConfig();
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 9e49b52cd9..b244c0ceff 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -1343,7 +1343,7 @@ public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL()
GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null);
Entity viewEntity = new(
Source: new("books_view_all", EntitySourceType.Table, null, null),
- Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
+ Rest: new(Enabled: true),
GraphQL: new("", ""),
Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: null,
@@ -1539,7 +1539,7 @@ public void TestInvalidDatabaseColumnNameHandling(
Entity entity = new(
Source: new("graphql_incompatible", EntitySourceType.Table, null, null),
- Rest: new(Array.Empty(), Enabled: false),
+ Rest: new(Enabled: false),
GraphQL: new("graphql_incompatible", "graphql_incompatibles", entityGraphQLEnabled),
Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: null,
@@ -1673,7 +1673,7 @@ public async Task OpenApi_GlobalEntityRestPath(bool globalRestEnabled, bool expe
// file creation function.
Entity requiredEntity = new(
Source: new("books", EntitySourceType.Table, null, null),
- Rest: new(Array.Empty(), Enabled: false),
+ Rest: new(Enabled: false),
GraphQL: new("book", "books"),
Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: null,
@@ -1727,7 +1727,7 @@ public async Task OpenApi_EntityLevelRestEndpoint()
// Create the entities under test.
Entity restEnabledEntity = new(
Source: new("books", EntitySourceType.Table, null, null),
- Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
+ Rest: new(Enabled: true),
GraphQL: new("", "", false),
Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: null,
@@ -1735,7 +1735,7 @@ public async Task OpenApi_EntityLevelRestEndpoint()
Entity restDisabledEntity = new(
Source: new("publishers", EntitySourceType.Table, null, null),
- Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Enabled: false),
+ Rest: new(Enabled: false),
GraphQL: new("publisher", "publishers", true),
Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: null,
@@ -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,
@@ -2171,7 +2172,7 @@ public static string GetConnectionStringFromEnvironmentConfig(string environment
string sqlFile = new FileSystemRuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true);
string configPayload = File.ReadAllText(sqlFile);
- RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig);
+ RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig, replaceEnvVar: true);
return runtimeConfig.DataSource.ConnectionString;
}
diff --git a/src/Service.Tests/CosmosTests/TestBase.cs b/src/Service.Tests/CosmosTests/TestBase.cs
index e53afd22cb..c43fed29e5 100644
--- a/src/Service.Tests/CosmosTests/TestBase.cs
+++ b/src/Service.Tests/CosmosTests/TestBase.cs
@@ -29,6 +29,8 @@ namespace Azure.DataApiBuilder.Service.Tests.CosmosTests;
public class TestBase
{
internal const string DATABASE_NAME = "graphqldb";
+ // Intentionally removed name attibute from Planet model to test scenario where the 'name' attribute
+ // is not explicitly added in the schema
internal const string GRAPHQL_SCHEMA = @"
type Character @model(name:""Character"") {
id : ID,
@@ -39,7 +41,7 @@ type Character @model(name:""Character"") {
star: Star
}
-type Planet @model(name:""Planet"") {
+type Planet @model {
id : ID!,
name : String,
character: Character,
diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql
index 38a4f9c329..3122169954 100644
--- a/src/Service.Tests/DatabaseSchema-MsSql.sql
+++ b/src/Service.Tests/DatabaseSchema-MsSql.sql
@@ -163,7 +163,7 @@ CREATE TABLE type_table(
datetimeoffset_types datetimeoffset,
smalldatetime_types smalldatetime,
bytearray_types varbinary(max),
- guid_types uniqueidentifier DEFAULT newid()
+ uuid_types uniqueidentifier DEFAULT newid()
);
CREATE TABLE trees (
@@ -414,6 +414,7 @@ VALUES
'9998-12-31', '9998-12-31 23:59:59', '9998-12-31 23:59:59.9999999', '9998-12-31 23:59:59.9999999+00:00', '2079-06-06',
0xFFFFFFFF),
(5, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO type_table(id, uuid_types) values(10, 'D1D021A8-47B4-4AE4-B718-98E89C41A161');
SET IDENTITY_INSERT type_table OFF
SET IDENTITY_INSERT sales ON
diff --git a/src/Service.Tests/DatabaseSchema-PostgreSql.sql b/src/Service.Tests/DatabaseSchema-PostgreSql.sql
index 091c5958b6..9c58d8f176 100644
--- a/src/Service.Tests/DatabaseSchema-PostgreSql.sql
+++ b/src/Service.Tests/DatabaseSchema-PostgreSql.sql
@@ -143,7 +143,7 @@ CREATE TABLE type_table(
boolean_types boolean,
datetime_types timestamp,
bytearray_types bytea,
- guid_types uuid DEFAULT gen_random_uuid ()
+ uuid_types uuid DEFAULT gen_random_uuid ()
);
CREATE TABLE trees (
@@ -328,6 +328,7 @@ INSERT INTO type_table(id, short_types, int_types, long_types, string_types, sin
(3, -32768, -2147483648, -9223372036854775808, '', -3.4E38, -1.7E308, 2.929292E-19, true, '1753-01-01 00:00:00.000', '\x00000000'),
(4, 32767, 2147483647, 9223372036854775807, 'null', 3.4E38, 1.7E308, 2.929292E-14, true, '9998-12-31 23:59:59.997', '\xFFFFFFFF'),
(5, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO type_table(id, uuid_types) values(10, 'D1D021A8-47B4-4AE4-B718-98E89C41A161');
INSERT INTO trees("treeId", species, region, height) VALUES (1, 'Tsuga terophylla', 'Pacific Northwest', '30m'), (2, 'Pseudotsuga menziesii', 'Pacific Northwest', '40m');
INSERT INTO fungi(speciesid, region) VALUES (1, 'northeast'), (2, 'southwest');
INSERT INTO notebooks(id, noteBookName, color, ownerName) VALUES (1, 'Notebook1', 'red', 'Sean'), (2, 'Notebook2', 'green', 'Ani'), (3, 'Notebook3', 'blue', 'Jarupat'), (4, 'Notebook4', 'yellow', 'Aaron');
diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
index 9172d4b116..f172f63719 100644
--- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
+++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
@@ -44,7 +44,7 @@ private static Entity GenerateEmptyEntity()
{
return new Entity(
Source: new("dbo.entity", EntitySourceType.Table, null, null),
- Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Enabled: false),
+ Rest: new(Enabled: false),
GraphQL: new("Foo", "Foos", Enabled: true),
Permissions: Array.Empty(),
Relationships: new(),
diff --git a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs
index 3a73c5bf61..d96b3e6c72 100644
--- a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs
+++ b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs
@@ -229,7 +229,7 @@ public void MultipleColumnsAllMapped()
[DataRow(typeof(DateTime), DATETIME_TYPE)]
[DataRow(typeof(DateTimeOffset), DATETIME_TYPE)]
[DataRow(typeof(byte[]), BYTEARRAY_TYPE)]
- [DataRow(typeof(Guid), STRING_TYPE)]
+ [DataRow(typeof(Guid), UUID_TYPE)]
public void SystemTypeMapsToCorrectGraphQLType(Type systemType, string graphQLType)
{
SourceDefinition table = new();
@@ -741,7 +741,7 @@ public static Entity GenerateEmptyEntity(string entityName)
{
return new Entity(
Source: new($"{SCHEMA_NAME}.{TABLE_NAME}", EntitySourceType.Table, null, null),
- Rest: new(new SupportedHttpVerb[] { }),
+ Rest: new(Enabled: true),
GraphQL: new(entityName, ""),
Permissions: Array.Empty(),
Relationships: new(),
diff --git a/src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs b/src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs
new file mode 100644
index 0000000000..21ff38fa5c
--- /dev/null
+++ b/src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Azure.DataApiBuilder.Config;
+using Azure.DataApiBuilder.Config.ObjectModel;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.Readers;
+
+namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration
+{
+ // Defines helpers used to help generate an OpenApiDocument object which
+ // can be validated in tests so that a constantly running DAB instance
+ // isn't necessary.
+ internal class OpenApiTestBootstrap
+ {
+ ///
+ /// Bootstraps a test server instance using a runtime config file generated
+ /// from the provided entity collection. The test server is only used to generate
+ /// and return the OpenApiDocument for use this method's callers.
+ ///
+ ///
+ ///
+ ///
+ /// Generated OpenApiDocument
+ internal static async Task GenerateOpenApiDocument(
+ RuntimeEntities runtimeEntities,
+ string configFileName,
+ string databaseEnvironment)
+ {
+ TestHelper.SetupDatabaseEnvironment(databaseEnvironment);
+ FileSystem fileSystem = new();
+ FileSystemRuntimeConfigLoader loader = new(fileSystem);
+ loader.TryLoadKnownConfig(out RuntimeConfig config);
+
+ RuntimeConfig configWithCustomHostMode = config with
+ {
+ Runtime = config.Runtime with
+ {
+ Host = config.Runtime.Host with { Mode = HostMode.Production }
+ },
+ Entities = runtimeEntities
+ };
+
+ File.WriteAllText(configFileName, configWithCustomHostMode.ToJson());
+ string[] args = new[]
+ {
+ $"--ConfigFileName={configFileName}"
+ };
+
+ using TestServer server = new(Program.CreateWebHostBuilder(args));
+ using HttpClient client = server.CreateClient();
+ {
+ HttpRequestMessage request = new(HttpMethod.Get, "/api/openapi");
+
+ HttpResponseMessage response = await client.SendAsync(request);
+ Stream responseStream = await response.Content.ReadAsStreamAsync();
+
+ // Read V3 as YAML
+ OpenApiDocument openApiDocument = new OpenApiStreamReader().Read(responseStream, out OpenApiDiagnostic diagnostic);
+
+ TestHelper.UnsetAllDABEnvironmentVariables();
+ return openApiDocument;
+ }
+ }
+
+ ///
+ /// Creates basic permissions collection with the anonymous and authenticated roles
+ /// where all actions are permitted.
+ ///
+ /// Array of EntityPermission objects.
+ internal static EntityPermission[] CreateBasicPermissions()
+ {
+ List permissions = new()
+ {
+ new EntityPermission(Role: "anonymous", Actions: new EntityAction[]
+ {
+ new(Action: EntityActionOperation.All, Fields: null, Policy: new())
+ }),
+ new EntityPermission(Role: "authenticated", Actions: new EntityAction[]
+ {
+ new(Action: EntityActionOperation.All, Fields: null, Policy: new())
+ })
+ };
+
+ return permissions.ToArray();
+ }
+ }
+}
diff --git a/src/Service.Tests/OpenApiDocumentor/PathValidation.cs b/src/Service.Tests/OpenApiDocumentor/PathValidation.cs
new file mode 100644
index 0000000000..5ebf8c0ee2
--- /dev/null
+++ b/src/Service.Tests/OpenApiDocumentor/PathValidation.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Azure.DataApiBuilder.Config.ObjectModel;
+using Microsoft.OpenApi.Models;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration
+{
+ ///
+ /// Integration tests validating correct keys are created for OpenApiDocument.Paths
+ /// which represent the path used to access an entity in DAB's REST API endpoint.
+ ///
+ [TestCategory(TestCategory.MSSQL)]
+ [TestClass]
+ public class PathValidation
+ {
+ private const string CUSTOM_CONFIG = "path-config.MsSql.json";
+ private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL;
+
+ // Error messages
+ private const string PATH_GENERATION_ERROR = "Unexpected path value for entity in OpenAPI description document: ";
+
+ ///
+ /// Validates that the OpenApiDocument object's Paths property for an entity is generated
+ /// with the entity's explicitly configured REST path, if set. Otherwise, the top level
+ /// entity name is used.
+ /// When OpenApiDocumentor.BuildPaths() is called, the entityBasePathComponent is created using
+ /// the formula "/{entityRestPath}" where {entityRestPath} has no starting slashes and is either
+ /// the entity name or the explicitly configured entity REST path
+ ///
+ /// Top level entity name defined in runtime config.
+ /// Entity's configured REST path.
+ /// Expected path generated for OpenApiDocument.Paths with format: "/{entityRestPath}"
+ [DataRow("entity", "/customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has leading slash - REST path override used.")]
+ [DataRow("entity", "//customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has two leading slashes - REST path override used.")]
+ [DataRow("entity", "///customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has many leading slashes - REST path override used.")]
+ [DataRow("entity", "customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has no leading slash(es) - REST path override used.")]
+ [DataRow("entity", "", "/entity", DisplayName = "Entity REST path is an emtpy string - top level entity name used.")]
+ [DataRow("entity", null, "/entity", DisplayName = "Entity REST path is null - top level entity name used.")]
+ [DataTestMethod]
+ public async Task ValidateEntityRestPath(string entityName, string configuredRestPath, string expectedOpenApiPath)
+ {
+ Entity entity = new(
+ Source: new(Object: "books", EntitySourceType.Table, null, null),
+ GraphQL: new(Singular: null, Plural: null, Enabled: false),
+ Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Path: configuredRestPath),
+ Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
+ Mappings: null,
+ Relationships: null);
+
+ Dictionary entities = new()
+ {
+ { entityName, entity }
+ };
+
+ RuntimeEntities runtimeEntities = new(entities);
+ OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument(
+ runtimeEntities: runtimeEntities,
+ configFileName: CUSTOM_CONFIG,
+ databaseEnvironment: MSSQL_ENVIRONMENT);
+
+ // For a given table backed entity, there will be two paths:
+ // 1. GetById path: "/customEntityPath/id/{id}"
+ // 2. GetAll path: "/customEntityPath"
+ foreach (string actualPath in openApiDocument.Paths.Keys)
+ {
+ Assert.AreEqual(expected: true, actual: actualPath.StartsWith(expectedOpenApiPath), message: PATH_GENERATION_ERROR + actualPath);
+ }
+ }
+ }
+}
diff --git a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs
index a671d90974..3c5d4b4b6b 100644
--- a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs
+++ b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs
@@ -2,18 +2,12 @@
// Licensed under the MIT License.
using System.Collections.Generic;
-using System.IO;
-using System.IO.Abstractions;
using System.Linq;
using System.Net;
-using System.Net.Http;
using System.Threading.Tasks;
-using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Services;
-using Microsoft.AspNetCore.TestHost;
using Microsoft.OpenApi.Models;
-using Microsoft.OpenApi.Readers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration
@@ -38,7 +32,7 @@ public class StoredProcedureGeneration
private static RuntimeEntities _runtimeEntities;
///
- /// Bootstraps test server once using a single runtime config file so
+ /// Bootstraps a single test server instance using one runtime config file so
/// each test need not boot the entire server to generate a description doc.
/// Each test validates the OpenAPI description generated for a distinct entity.
///
@@ -46,41 +40,11 @@ public class StoredProcedureGeneration
[ClassInitialize]
public static async Task ClassInitialize(TestContext context)
{
- TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT);
- FileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- loader.TryLoadKnownConfig(out RuntimeConfig config);
CreateEntities();
-
- RuntimeConfig configWithCustomHostMode = config with
- {
- Runtime = config.Runtime with
- {
- Host = config.Runtime.Host with { Mode = HostMode.Production }
- },
- Entities = _runtimeEntities
- };
-
- File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson());
- string[] args = new[]
- {
- $"--ConfigFileName={CUSTOM_CONFIG}"
- };
-
- using TestServer server = new(Program.CreateWebHostBuilder(args));
- using HttpClient client = server.CreateClient();
- {
- HttpRequestMessage request = new(HttpMethod.Get, "/api/openapi");
-
- HttpResponseMessage response = await client.SendAsync(request);
- Stream responseStream = await response.Content.ReadAsStreamAsync();
-
- // Read V3 as YAML
- OpenApiDocument openApiDocument = new OpenApiStreamReader().Read(responseStream, out OpenApiDiagnostic diagnostic);
- _openApiDocument = openApiDocument;
-
- TestHelper.UnsetAllDABEnvironmentVariables();
- }
+ _openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument(
+ runtimeEntities: _runtimeEntities,
+ configFileName: CUSTOM_CONFIG,
+ databaseEnvironment: MSSQL_ENVIRONMENT);
}
///
@@ -93,7 +57,7 @@ public static void CreateEntities()
Source: new(Object: "insert_and_display_all_books_for_given_publisher", EntitySourceType.StoredProcedure, null, null),
GraphQL: new(Singular: null, Plural: null, Enabled: false),
Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
- Permissions: CreateBasicPermissions(),
+ Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
Mappings: null,
Relationships: null);
@@ -105,28 +69,6 @@ public static void CreateEntities()
_runtimeEntities = new(entities);
}
- ///
- /// Creates basic permissions collection with the anonymous and authenticated roles
- /// where all actions are permitted.
- ///
- /// Array of EntityPermission objects.
- private static EntityPermission[] CreateBasicPermissions()
- {
- List permissions = new()
- {
- new EntityPermission("anonymous", new EntityAction[]
- {
- new(EntityActionOperation.Execute, null, new())
- }),
- new EntityPermission("authenticated", new EntityAction[]
- {
- new(EntityActionOperation.Execute, null, new())
- })
- };
-
- return permissions.ToArray();
- }
-
///
/// Validates that the generated request body references stored procedure parameters
/// and not result set columns.
diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt
index 5489d3a593..d4b248e510 100644
--- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt
+++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt
@@ -1546,13 +1546,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt
index 597297b377..e701eb2699 100644
--- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt
+++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt
@@ -1409,13 +1409,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt
index 750876f7c4..4f127dee0a 100644
--- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt
+++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt
@@ -1325,13 +1325,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
@@ -1394,13 +1387,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
@@ -1431,13 +1417,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
@@ -1467,13 +1446,6 @@
Enabled: true
},
Rest: {
- Methods: [
- Get,
- Post,
- Put,
- Patch,
- Delete
- ],
Enabled: true
},
Permissions: [
diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs
index 96c65c2bcf..c7c7b9624e 100644
--- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs
+++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs
@@ -1638,7 +1638,7 @@ public async Task TestConfigTakesPrecedenceForRelationshipFieldsOverDB(
Entity clubEntity = new(
Source: new("clubs", EntitySourceType.Table, null, null),
- Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
+ Rest: new(Enabled: true),
GraphQL: new("club", "clubs"),
Permissions: new[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: null,
@@ -1647,7 +1647,7 @@ public async Task TestConfigTakesPrecedenceForRelationshipFieldsOverDB(
Entity playerEntity = new(
Source: new("players", EntitySourceType.Table, null, null),
- Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
+ Rest: new(Enabled: true),
GraphQL: new("player", "players"),
Permissions: new[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) },
Relationships: new Dictionary() { {"clubs", new (
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs
index 7936566f72..a330dc00fd 100644
--- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs
@@ -64,10 +64,10 @@ public abstract class GraphQLSupportedTypesTestBase : SqlTestBase
[DataRow(BYTEARRAY_TYPE, 2)]
[DataRow(BYTEARRAY_TYPE, 3)]
[DataRow(BYTEARRAY_TYPE, 4)]
- [DataRow(GUID_TYPE, 1)]
- [DataRow(GUID_TYPE, 2)]
- [DataRow(GUID_TYPE, 3)]
- [DataRow(GUID_TYPE, 4)]
+ [DataRow(UUID_TYPE, 1)]
+ [DataRow(UUID_TYPE, 2)]
+ [DataRow(UUID_TYPE, 3)]
+ [DataRow(UUID_TYPE, 4)]
public async Task QueryTypeColumn(string type, int id)
{
if (!IsSupportedType(type))
@@ -134,6 +134,8 @@ public async Task QueryTypeColumn(string type, int id)
[DataRow(DECIMAL_TYPE, "eq", "-9.292929", "-9.292929", "=")]
[DataRow(BOOLEAN_TYPE, "neq", "\'false\'", "false", "!=")]
[DataRow(BOOLEAN_TYPE, "eq", "\'false\'", "false", "=")]
+ [DataRow(UUID_TYPE, "eq", "'D1D021A8-47B4-4AE4-B718-98E89C41A161'", "\"D1D021A8-47B4-4AE4-B718-98E89C41A161\"", "=")]
+ [DataRow(UUID_TYPE, "neq", "'D1D021A8-47B4-4AE4-B718-98E89C41A161'", "\"D1D021A8-47B4-4AE4-B718-98E89C41A161\"", "!=")]
public async Task QueryTypeColumnFilterAndOrderBy(string type, string filterOperator, string sqlValue, string gqlValue, string queryOperator)
{
if (!IsSupportedType(type))
@@ -340,8 +342,8 @@ public async Task InsertIntoTypeColumnWithArgument(string type, object value)
[DataRow(BYTEARRAY_TYPE, "\"U3RyaW5neQ==\"")]
[DataRow(BYTEARRAY_TYPE, "\"V2hhdGNodSBkb2luZyBkZWNvZGluZyBvdXIgdGVzdCBiYXNlNjQgc3RyaW5ncz8=\"")]
[DataRow(BYTEARRAY_TYPE, "null")]
- [DataRow(GUID_TYPE, "\"3a1483a5-9ac2-4998-bcf3-78a28078c6ac\"")]
- [DataRow(GUID_TYPE, "null")]
+ [DataRow(UUID_TYPE, "\"3a1483a5-9ac2-4998-bcf3-78a28078c6ac\"")]
+ [DataRow(UUID_TYPE, "null")]
public async Task UpdateTypeColumn(string type, string value)
{
if (!IsSupportedType(type))
@@ -382,8 +384,8 @@ public async Task UpdateTypeColumn(string type, string value)
[DataRow(DATETIME_TYPE, "1999-01-08 10:23:54")]
[DataRow(DATETIME_NONUTC_TYPE, "1999-01-08 10:23:54+8:00")]
[DataRow(BYTEARRAY_TYPE, "V2hhdGNodSBkb2luZyBkZWNvZGluZyBvdXIgdGVzdCBiYXNlNjQgc3RyaW5ncz8=")]
- [DataRow(GUID_TYPE, "3a1483a5-9ac2-4998-bcf3-78a28078c6ac")]
- [DataRow(GUID_TYPE, null)]
+ [DataRow(UUID_TYPE, "3a1483a5-9ac2-4998-bcf3-78a28078c6ac")]
+ [DataRow(UUID_TYPE, null)]
public async Task UpdateTypeColumnWithArgument(string type, object value)
{
if (!IsSupportedType(type))
@@ -400,7 +402,7 @@ public async Task UpdateTypeColumnWithArgument(string type, object value)
string field = $"{type.ToLowerInvariant()}_types";
string graphQLQueryName = "updateSupportedType";
- string gqlQuery = "mutation($param: " + TypeNameToGraphQLType(type) + "){ updateSupportedType (typeid: 1, item: {" + field + ": $param }){ " + field + " } }";
+ string gqlQuery = "mutation($param: " + type + "){ updateSupportedType (typeid: 1, item: {" + field + ": $param }){ " + field + " } }";
string dbQuery = MakeQueryOnTypeTable(new List { field }, id: 1);
@@ -428,12 +430,45 @@ private static void PerformTestEqualsForExtendedTypes(string type, string expect
{
CompareDateTimeResults(actual.ToString(), expected);
}
+ else if (type == UUID_TYPE)
+ {
+ CompareUuidResults(actual.ToString(), expected);
+ }
else
{
SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString());
}
}
+ private static void CompareUuidResults(string actual, string expected)
+ {
+ string fieldName = "uuid_types";
+
+ using JsonDocument actualJsonDoc = JsonDocument.Parse(actual);
+ using JsonDocument expectedJsonDoc = JsonDocument.Parse(expected);
+
+ if (actualJsonDoc.RootElement.ValueKind is JsonValueKind.Array)
+ {
+ ValidateArrayResults(actualJsonDoc, expectedJsonDoc, fieldName);
+ return;
+ }
+
+ string actualUuidString = actualJsonDoc.RootElement.GetProperty(fieldName).ToString();
+ string expectedUuidString = expectedJsonDoc.RootElement.GetProperty(fieldName).ToString();
+
+ // handles cases when one of the values is null
+ if (string.IsNullOrEmpty(actualUuidString) || string.IsNullOrEmpty(expectedUuidString))
+ {
+ Assert.AreEqual(expectedUuidString, actualUuidString);
+ }
+ else
+ {
+ Guid actualGuidValue = Guid.Parse(actualUuidString);
+ Guid expectedGuidValue = Guid.Parse(expectedUuidString);
+ Assert.AreEqual(actualGuidValue, expectedGuidValue);
+ }
+ }
+
///
/// HotChocolate will parse large floats to exponential notation
/// while the db will return the number fully printed out. Because
@@ -530,6 +565,12 @@ private static void ValidateArrayResults(JsonDocument actualJsonDoc, JsonDocumen
DateTime expectedDateTime = DateTime.Parse(expectedValue.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.None);
Assert.AreEqual(expectedDateTime, actualDateTime);
}
+ else if (fieldName.StartsWith(UUID_TYPE.ToLower()))
+ {
+ Guid actualGuidValue = Guid.Parse(actualValue.ToString());
+ Guid expectedGuidValue = Guid.Parse(expectedValue.ToString());
+ Assert.AreEqual(expectedGuidValue, actualGuidValue);
+ }
else if (fieldName.StartsWith(SINGLE_TYPE.ToLower()))
{
Assert.AreEqual(expectedValue.GetSingle(), actualValue.GetSingle());
@@ -541,20 +582,6 @@ private static void ValidateArrayResults(JsonDocument actualJsonDoc, JsonDocumen
}
}
- ///
- /// Needed to map the type name to a graphql type in argument tests
- /// where the argument type need to be specified.
- ///
- private static string TypeNameToGraphQLType(string typeName)
- {
- if (typeName is GUID_TYPE)
- {
- return STRING_TYPE;
- }
-
- return typeName;
- }
-
protected abstract string MakeQueryOnTypeTable(
List queriedColumns,
string filterValue = "1",
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs
index bc45b12e28..3ed80e425b 100644
--- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs
@@ -56,7 +56,7 @@ protected override bool IsSupportedType(string type)
{
return type switch
{
- GUID_TYPE => false,
+ UUID_TYPE => false,
_ => true
};
}
diff --git a/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs
index 2d7b8e2d70..3ebe82b021 100644
--- a/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs
+++ b/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs
@@ -32,7 +32,7 @@ public class MsSqlInsertApiTests : InsertApiTestBase
"InsertOneInSupportedTypes",
$"SELECT [id] as [typeid], [byte_types], [short_types], [int_types], [long_types],string_types, [single_types], [float_types], " +
$"[decimal_types], [boolean_types], [date_types], [datetime_types], [datetime2_types], [datetimeoffset_types], [smalldatetime_types], " +
- $"[bytearray_types], LOWER([guid_types]) as [guid_types] FROM { _integrationTypeTable } " +
+ $"[bytearray_types], LOWER([uuid_types]) as [uuid_types] FROM { _integrationTypeTable } " +
$"WHERE [id] = { STARTING_ID_FOR_TEST_INSERTS } AND [bytearray_types] is NULL " +
$"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER"
},
diff --git a/src/Service.Tests/SqlTests/RestApiTests/Insert/PostgreSqlInsertApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Insert/PostgreSqlInsertApiTests.cs
index e7ec4549f2..8bc4840673 100644
--- a/src/Service.Tests/SqlTests/RestApiTests/Insert/PostgreSqlInsertApiTests.cs
+++ b/src/Service.Tests/SqlTests/RestApiTests/Insert/PostgreSqlInsertApiTests.cs
@@ -34,7 +34,7 @@ SELECT to_jsonb(subq) AS data
SELECT to_jsonb(subq) AS data
FROM (
SELECT id as typeid, short_types, int_types, long_types, string_types, single_types,
- float_types, decimal_types, boolean_types, datetime_types, bytearray_types, guid_types
+ float_types, decimal_types, boolean_types, datetime_types, bytearray_types, uuid_types
FROM " + _integrationTypeTable + @"
WHERE id = " + STARTING_ID_FOR_TEST_INSERTS + @"
) AS subq
diff --git a/src/Service.Tests/TestHelper.cs b/src/Service.Tests/TestHelper.cs
index a6eb8e0d30..507c81610b 100644
--- a/src/Service.Tests/TestHelper.cs
+++ b/src/Service.Tests/TestHelper.cs
@@ -74,7 +74,7 @@ public static RuntimeConfig AddMissingEntitiesToConfig(RuntimeConfig config, str
Entity entity = new(
Source: new(entityName, EntitySourceType.Table, null, null),
GraphQL: new(entityKey, entityKey.Pluralize()),
- Rest: new(Array.Empty()),
+ Rest: new(Enabled: true),
Permissions: new[]
{
new EntityPermission("anonymous", new EntityAction[] {
diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs
index 62a4ceebc8..5bce00fc1c 100644
--- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs
+++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs
@@ -2,8 +2,10 @@
// Licensed under the MIT License.
#nullable disable
+using System;
using System.Collections.Generic;
using System.Data;
+using System.IO;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Net;
@@ -110,7 +112,7 @@ public void InvalidCRUDForStoredProcedure(
Entity testEntity = new(
Source: entitySource,
- Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
+ Rest: new(EntityRestOptions.DEFAULT_HTTP_VERBS_ENABLED_FOR_SP),
GraphQL: new(AuthorizationHelpers.TEST_ENTITY, AuthorizationHelpers.TEST_ENTITY + "s"),
Permissions: permissionSettings.ToArray(),
Relationships: null,
@@ -123,10 +125,7 @@ public void InvalidCRUDForStoredProcedure(
) with
{ Entities = new(new Dictionary() { { AuthorizationHelpers.TEST_ENTITY, testEntity } }) };
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
try
{
@@ -162,10 +161,8 @@ public void InvalidActionSpecifiedForARole(string dbPolicy, EntityActionOperatio
includedCols: new HashSet { "col1", "col2", "col3" },
databasePolicy: dbPolicy
);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
// Assert that expected exception is thrown.
DataApiBuilderException ex = Assert.ThrowsException(() => RuntimeConfigValidator.ValidatePermissionsInConfig(runtimeConfig));
@@ -259,10 +256,7 @@ public void TestAddingRelationshipWithInvalidTargetEntity()
Entities: new(entityMap)
);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
Mock _sqlMetadataProvider = new();
// Assert that expected exception is thrown. Entity used in relationship is Invalid
@@ -323,10 +317,7 @@ public void TestAddingRelationshipWithDisabledGraphQL()
Entities: new(entityMap)
);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
Mock _sqlMetadataProvider = new();
// Exception should be thrown as we cannot use an entity (with graphQL disabled) in a relationship.
@@ -637,10 +628,8 @@ public void EmptyClaimTypeSuppliedInPolicy(string dbPolicy)
includedCols: new HashSet { "col1", "col2", "col3" },
databasePolicy: dbPolicy
);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
// Assert that expected exception is thrown.
DataApiBuilderException ex = Assert.ThrowsException(() =>
@@ -672,10 +661,8 @@ public void ParseInvalidDbPolicyWithInvalidClaimTypeFormat(string policy)
includedCols: new HashSet { "col1", "col2", "col3" },
databasePolicy: policy
);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
// Assert that expected exception is thrown.
DataApiBuilderException ex = Assert.ThrowsException(() =>
@@ -754,10 +741,8 @@ public void WildCardAndOtherFieldsPresentInIncludeSet(EntityActionOperation acti
operation: actionOp,
includedCols: new HashSet { "*", "col2" }
);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
// Assert that expected exception is thrown.
DataApiBuilderException ex = Assert.ThrowsException(() =>
@@ -780,10 +765,8 @@ public void WildCardAndOtherFieldsPresentInExcludeSet(EntityActionOperation acti
operation: actionOp,
excludedCols: new HashSet { "*", "col1" }
);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
// Assert that expected exception is thrown.
DataApiBuilderException ex = Assert.ThrowsException(() =>
@@ -854,10 +837,8 @@ public void TestOperationValidityAndCasing(string operationName, bool exceptionE
),
Entities: new(entityMap));
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
+
if (!exceptionExpected)
{
RuntimeConfigValidator.ValidatePermissionsInConfig(runtimeConfig);
@@ -929,10 +910,12 @@ public void ValidateGraphQLTypeNamesFromConfig(string entityNameFromConfig, bool
Entities: new(entityMap)
);
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
+
if (expectsException)
{
DataApiBuilderException dabException = Assert.ThrowsException(
- action: () => RuntimeConfigValidator.ValidateEntityConfiguration(runtimeConfig),
+ action: () => configValidator.ValidateEntityConfiguration(runtimeConfig),
message: $"Entity name \"{entityNameFromConfig}\" incorrectly passed validation.");
Assert.AreEqual(expected: HttpStatusCode.ServiceUnavailable, actual: dabException.StatusCode);
@@ -940,7 +923,7 @@ public void ValidateGraphQLTypeNamesFromConfig(string entityNameFromConfig, bool
}
else
{
- RuntimeConfigValidator.ValidateEntityConfiguration(runtimeConfig);
+ configValidator.ValidateEntityConfiguration(runtimeConfig);
}
}
@@ -1344,7 +1327,7 @@ private static Entity GetSampleEntityUsingSourceAndRelationshipMap(
Entity sampleEntity = new(
Source: new(source, EntitySourceType.Table, null, null),
- Rest: restDetails ?? new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Enabled: false),
+ Rest: restDetails ?? new(Enabled: false),
GraphQL: graphQLDetails,
Permissions: new[] { permissionForEntity },
Relationships: relationshipMap,
@@ -1660,10 +1643,7 @@ public void TestFieldInclusionExclusion(
}";
RuntimeConfigLoader.TryParseConfig(runtimeConfigString, out RuntimeConfig runtimeConfig);
- MockFileSystem fileSystem = new();
- FileSystemRuntimeConfigLoader loader = new(fileSystem);
- RuntimeConfigProvider provider = new(loader);
- RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object);
+ RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
// Perform validation on the permissions in the config and assert the expected results.
if (exceptionExpected)
@@ -1778,88 +1758,64 @@ public void ValidateMisconfiguredColumnSets(
}
///
- /// Test to validate that the rest methods are correctly configured for entities in the config.
- /// Rest methods can only be configured for stored procedures as an array of valid REST operations.
+ /// Validates that a warning is logged when REST methods are configured for tables and views. For stored procedures,
+ /// no warnings are logged.
///
/// The source type of the entity.
/// Value of the rest methods property configured for the entity.
/// Boolean value representing whether an exception is expected or not.
/// Expected error message when an exception is expected for the test run.
- [Ignore]
[DataTestMethod]
- [DataRow(EntitySourceType.Table, "[\"get\"]", true,
- $"The rest property 'methods' is present for entity: HybridEntity of type: Table, but is only valid for type: StoredProcedure.",
- DisplayName = "Rest methods specified for non-storedprocedure entity fail config validation.")]
- [DataRow(EntitySourceType.StoredProcedure, "[\"Get\", \"post\", \"PUT\", \"paTch\", \"delete\"]", false,
- DisplayName = "Valid rest operations specified in rest methods for stored procedure pass config validation.")]
+ [DataRow(EntitySourceType.Table, new SupportedHttpVerb[] { SupportedHttpVerb.Get, SupportedHttpVerb.Post }, true,
+ DisplayName = "Tables with REST Methods configured - Engine logs a warning during startup")]
+ [DataRow(EntitySourceType.StoredProcedure, new SupportedHttpVerb[] { SupportedHttpVerb.Get, SupportedHttpVerb.Post }, false,
+ DisplayName = "Stored Procedures with REST Methods configured - No warnings logged")]
public void ValidateRestMethodsForEntityInConfig(
EntitySourceType sourceType,
- string methods,
- bool exceptionExpected,
- string expectedErrorMessage = "")
+ SupportedHttpVerb[] methods,
+ bool isWarningLogExpected)
{
- string runtimeConfigString = @"{
- " +
- @"""$schema"": ""test_schema""," +
- @"""data-source"": {
- ""database-type"": ""mssql"",
- ""connection-string"": ""testconnectionstring"",
- ""options"":{
- ""set-session-context"": false
- }
- },
- ""runtime"": {
- ""host"": {
- ""mode"": ""development"",
- ""authentication"": {
- ""provider"": ""StaticWebApps""
- }
- },
- ""rest"": {
- ""enabled"": true,
- ""path"": ""/api""
- },
- ""graphql"": {
- ""enabled"": true,
- ""path"": ""/graphql"",
- ""allow-introspection"": true
- }
- },
- ""entities"": {
- ""HybridEntity"":{
- ""source"": {
- ""object"": ""hybridSource"",
- ""type"":" + $"\"{sourceType}\"" + @"
- },
- ""permissions"": [
- {
- ""role"": ""anonymous"",
- ""actions"": [
- ""*""
- ]
- }
- ],
- ""rest"":{
- ""methods"":" + $"{methods}" + @"
- }
- }
- }
- }";
+ Dictionary entityMap = new();
+ string entityName = "EntityA";
+ // Sets REST method for the entity
+ Entity entity = new(Source: new("TEST_SOURCE", sourceType, null, null),
+ Rest: new(Methods: methods),
+ GraphQL: new(entityName, ""),
+ Permissions: Array.Empty(),
+ Relationships: new(),
+ Mappings: new());
+ entityMap.Add(entityName, entity);
- RuntimeConfigLoader.TryParseConfig(runtimeConfigString, out RuntimeConfig runtimeConfig);
+ RuntimeConfig runtimeConfig = new(
+ Schema: "UnitTestSchema",
+ DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, string.Empty, Options: null),
+ Runtime: new(
+ Rest: new(),
+ GraphQL: new(),
+ Host: new(null, null)),
+ Entities: new(entityMap));
- // Perform validation on the entity in the config and assert the expected results.
- if (exceptionExpected)
- {
- DataApiBuilderException ex =
- Assert.ThrowsException(() => RuntimeConfigValidator.ValidateEntityConfiguration(runtimeConfig));
- Assert.AreEqual(expectedErrorMessage, ex.Message);
- Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
- Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode);
- }
- else
+ MockFileSystem fileSystem = new();
+ FileSystemRuntimeConfigLoader loader = new(fileSystem);
+ RuntimeConfigProvider provider = new(loader);
+ Mock> loggerMock = new();
+ RuntimeConfigValidator configValidator = new(provider, fileSystem, loggerMock.Object);
+
+ configValidator.ValidateEntityConfiguration(runtimeConfig);
+
+ if (isWarningLogExpected)
{
- RuntimeConfigValidator.ValidateEntityConfiguration(runtimeConfig);
+ // Assert on the log message to verify the warning log
+ loggerMock.Verify(
+ x => x.Log(
+ LogLevel.Warning,
+ It.IsAny(),
+ It.Is((o, t) => o.ToString()!.Equals($"Entity {entityName} has rest methods configured but is not a stored procedure. Values configured will be ignored and all 5 HTTP actions will be enabled.")),
+ It.IsAny(),
+ (Func)It.IsAny