Skip to content

Commit e560142

Browse files
Aniruddh25seantleonardabhishekkumamsneeraj-sharma2592Neeraj Sharma (from Dev Box)
authored
[Ports] Cherry-pick few bug fixes to 0.9 (#1879)
## Why make this change? This is simply cherry-picking the following PRs before creating the next stable 0.9 version: - #1876 - #1821 - #1854 - #1851 - #1872 - #1868 --------- Co-authored-by: Sean Leonard <[email protected]> Co-authored-by: Abhishek Kumar <[email protected]> Co-authored-by: neeraj-sharma2592 <[email protected]> Co-authored-by: Neeraj Sharma (from Dev Box) <[email protected]> Co-authored-by: aaronburtle <[email protected]>
1 parent 529471b commit e560142

File tree

14 files changed

+257
-209
lines changed

14 files changed

+257
-209
lines changed

.pipelines/cosmos-pipelines.yml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@ strategy:
1818
linux:
1919
imageName: "ubuntu-latest"
2020
ADDITIONAL_TEST_ARGS: '--collect "XPlat Code coverage"'
21-
mac:
22-
imageName: "macOS-latest"
23-
ADDITIONAL_TEST_ARGS: ''
2421
windows:
25-
imageName: "windows-2022"
22+
imageName: "windows-latest"
2623
ADDITIONAL_TEST_ARGS: '--collect "XPlat Code coverage"'
2724
maxParallel: 3
2825
pool:
@@ -32,6 +29,15 @@ variables:
3229
buildPlatform: 'Any CPU'
3330
buildConfiguration: 'Release'
3431
steps:
32+
- task: PowerShell@2
33+
displayName: Start CosmosDB Emulator
34+
condition: eq( variables['Agent.OS'], 'Windows_NT' )
35+
inputs:
36+
targetType: 'inline'
37+
script: |
38+
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
39+
Start-CosmosDbEmulator
40+
3541
- task: NuGetAuthenticate@0
3642
displayName: 'NuGet Authenticate'
3743

@@ -57,7 +63,18 @@ steps:
5763
projects: '**/*.csproj'
5864
arguments: '-p:generateConfigFileForDbType=cosmosdb_nosql --configuration $(buildConfiguration)' # Update this to match your need
5965

66+
- bash: |
67+
echo "Waiting for CosmosDB Emulator to Start"
68+
until [ "$(curl -k -s --connect-timeout 5 -o /dev/null -w "%{http_code}" https://localhost:8081/_explorer/index.html)" == "200" ]; do
69+
sleep 5;
70+
echo "Waiting for CosmosDB Emulator to Start"
71+
done;
72+
echo "CosmosDB Emulator is available"
73+
condition: eq( variables['Agent.OS'], 'Windows_NT' )
74+
displayName: Check CosmosDB Emulator is running
75+
6076
77+
condition: eq( variables['Agent.OS'], 'Linux' )
6178
displayName: 'Generate dab-config.CosmosDb_NoSql.json'
6279
inputs:
6380
folderPath: '$(System.DefaultWorkingDirectory)'

src/Core/Resolvers/SqlPaginationUtil.cs

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,37 @@ public static JsonDocument CreatePaginationConnectionFromJsonDocument(JsonDocume
114114
return result;
115115
}
116116

117+
/// <summary>
118+
/// Holds the information safe to expose in the response's pagination cursor,
119+
/// the NextLink. The NextLink column represents the safe to expose information
120+
/// that defines the entity, field, field value, and direction of sorting to
121+
/// continue to the next page. These can then be used to form the pagination
122+
/// columns that will be needed for the actual query.
123+
/// </summary>
124+
protected class NextLinkField
125+
{
126+
public string EntityName { get; set; }
127+
public string FieldName { get; set; }
128+
public object? FieldValue { get; }
129+
public string? ParamName { get; set; }
130+
public OrderBy Direction { get; set; }
131+
132+
public NextLinkField(
133+
string entityName,
134+
string fieldName,
135+
object? fieldValue,
136+
string? paramName = null,
137+
// default sorting direction is ascending so we maintain that convention
138+
OrderBy direction = OrderBy.ASC)
139+
{
140+
EntityName = entityName;
141+
FieldName = fieldName;
142+
FieldValue = fieldValue;
143+
ParamName = paramName;
144+
Direction = direction;
145+
}
146+
}
147+
117148
/// <summary>
118149
/// Extracts the columns from the JsonElement needed for pagination, represents them as a string in json format and base64 encodes.
119150
/// The JSON is encoded in base64 for opaqueness. The cursor should function as a token that the user copies and pastes
@@ -128,7 +159,7 @@ public static string MakeCursorFromJsonElement(
128159
string tableName = "",
129160
ISqlMetadataProvider? sqlMetadataProvider = null)
130161
{
131-
List<PaginationColumn> cursorJson = new();
162+
List<NextLinkField> cursorJson = new();
132163
JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
133164
// Hash set is used here to maintain linear runtime
134165
// in the worst case for this function. If list is used
@@ -148,12 +179,11 @@ public static string MakeCursorFromJsonElement(
148179
string? exposedColumnName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);
149180
if (TryResolveJsonElementToScalarVariable(element.GetProperty(exposedColumnName), out object? value))
150181
{
151-
cursorJson.Add(new PaginationColumn(tableSchema: schemaName,
152-
tableName: tableName,
153-
exposedColumnName,
154-
value,
155-
tableAlias: null,
156-
direction: column.Direction));
182+
cursorJson.Add(new NextLinkField(
183+
entityName: entityName,
184+
fieldName: exposedColumnName,
185+
fieldValue: value,
186+
direction: column.Direction));
157187
}
158188
else
159189
{
@@ -181,11 +211,10 @@ public static string MakeCursorFromJsonElement(
181211
string? exposedColumnName = GetExposedColumnName(entityName, column, sqlMetadataProvider);
182212
if (TryResolveJsonElementToScalarVariable(element.GetProperty(exposedColumnName), out object? value))
183213
{
184-
cursorJson.Add(new PaginationColumn(tableSchema: schemaName,
185-
tableName: tableName,
186-
exposedColumnName,
187-
value,
188-
direction: OrderBy.ASC));
214+
cursorJson.Add(new NextLinkField(
215+
entityName: entityName,
216+
fieldName: exposedColumnName,
217+
fieldValue: value));
189218
}
190219
else
191220
{
@@ -233,45 +262,54 @@ public static IEnumerable<PaginationColumn> ParseAfterFromQueryParams(
233262
/// Validate the value associated with $after, and return list of orderby columns
234263
/// it represents.
235264
/// </summary>
236-
public static IEnumerable<PaginationColumn> ParseAfterFromJsonString(string afterJsonString,
237-
PaginationMetadata paginationMetadata,
238-
ISqlMetadataProvider sqlMetadataProvider,
239-
string entityName,
240-
RuntimeConfigProvider runtimeConfigProvider
241-
)
265+
public static IEnumerable<PaginationColumn> ParseAfterFromJsonString(
266+
string afterJsonString,
267+
PaginationMetadata paginationMetadata,
268+
ISqlMetadataProvider sqlMetadataProvider,
269+
string entityName,
270+
RuntimeConfigProvider runtimeConfigProvider
271+
)
242272
{
243-
IEnumerable<PaginationColumn>? after;
273+
List<PaginationColumn>? paginationCursorColumnsForQuery = new();
274+
IEnumerable<NextLinkField>? paginationCursorFieldsFromRequest;
244275
try
245276
{
246277
afterJsonString = Base64Decode(afterJsonString);
247-
after = JsonSerializer.Deserialize<IEnumerable<PaginationColumn>>(afterJsonString);
278+
paginationCursorFieldsFromRequest = JsonSerializer.Deserialize<IEnumerable<NextLinkField>>(afterJsonString);
248279

249-
if (after is null)
280+
if (paginationCursorFieldsFromRequest is null)
250281
{
251282
throw new ArgumentException("Failed to parse the pagination information from the provided token");
252283
}
253284

254-
Dictionary<string, PaginationColumn> afterDict = new();
255-
foreach (PaginationColumn column in after)
285+
Dictionary<string, PaginationColumn> exposedFieldNameToBackingColumn = new();
286+
foreach (NextLinkField field in paginationCursorFieldsFromRequest)
256287
{
257288
// REST calls this function with a non null sqlMetadataProvider
258289
// which will get the exposed name for safe messaging in the response.
259290
// Since we are looking for pagination columns from the $after query
260291
// param, we expect this column to exist as the $after query param
261292
// was formed from a previous response with a nextLink. If the nextLink
262293
// has been modified and backingColumn is null we throw exception.
263-
string backingColumnName = GetBackingColumnName(entityName, column.ColumnName, sqlMetadataProvider);
294+
string backingColumnName = GetBackingColumnName(entityName, field.FieldName, sqlMetadataProvider);
264295
if (backingColumnName is null)
265296
{
266-
throw new DataApiBuilderException(message: $"Cursor for Pagination Predicates is not well formed, {column.ColumnName} is not valid.",
267-
statusCode: HttpStatusCode.BadRequest,
268-
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
297+
throw new DataApiBuilderException(
298+
message: $"Pagination token is not well formed because {field.FieldName} is not valid.",
299+
statusCode: HttpStatusCode.BadRequest,
300+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
269301
}
270302

303+
PaginationColumn pageColumn = new(
304+
tableName: "",
305+
tableSchema: "",
306+
columnName: backingColumnName,
307+
value: field.FieldValue,
308+
paramName: field.ParamName,
309+
direction: field.Direction);
310+
paginationCursorColumnsForQuery.Add(pageColumn);
271311
// holds exposed name mapped to exposed pagination column
272-
afterDict.Add(column.ColumnName, column);
273-
// overwrite with backing column's name for query generation
274-
column.ColumnName = backingColumnName;
312+
exposedFieldNameToBackingColumn.Add(field.FieldName, pageColumn);
275313
}
276314

277315
// verify that primary keys is a sub set of after's column names
@@ -284,12 +322,13 @@ RuntimeConfigProvider runtimeConfigProvider
284322
// which will get the exposed name for safe messaging in the response.
285323
// Since we are looking for primary keys we expect these columns to
286324
// exist.
287-
string safePK = GetExposedColumnName(entityName, pk, sqlMetadataProvider);
288-
if (!afterDict.ContainsKey(safePK))
325+
string exposedFieldName = GetExposedColumnName(entityName, pk, sqlMetadataProvider);
326+
if (!exposedFieldNameToBackingColumn.ContainsKey(exposedFieldName))
289327
{
290-
throw new DataApiBuilderException(message: $"Cursor for Pagination Predicates is not well formed, missing primary key column: {safePK}",
291-
statusCode: HttpStatusCode.BadRequest,
292-
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
328+
throw new DataApiBuilderException(
329+
message: $"Pagination token is not well formed because it is missing an expected field: {exposedFieldName}",
330+
statusCode: HttpStatusCode.BadRequest,
331+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
293332
}
294333
}
295334

@@ -299,28 +338,28 @@ RuntimeConfigProvider runtimeConfigProvider
299338
SqlQueryStructure structure = paginationMetadata.Structure!;
300339
foreach (OrderByColumn column in structure.OrderByColumns)
301340
{
302-
string columnName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);
341+
string exposedFieldName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);
303342

304-
if (!afterDict.ContainsKey(columnName) ||
305-
afterDict[columnName].Direction != column.Direction)
343+
if (!exposedFieldNameToBackingColumn.ContainsKey(exposedFieldName) ||
344+
exposedFieldNameToBackingColumn[exposedFieldName].Direction != column.Direction)
306345
{
307346
// REST calls this function with a non null sqlMetadataProvider
308347
// which will get the exposed name for safe messaging in the response.
309348
// Since we are looking for valid orderby columns we expect
310349
// these columns to exist.
311-
string safeColumnName = GetExposedColumnName(entityName, columnName, sqlMetadataProvider);
350+
string exposedOrderByFieldName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);
312351
throw new DataApiBuilderException(
313-
message: $"Could not match order by column {safeColumnName} with a column in the pagination token with the same name and direction.",
314-
statusCode: HttpStatusCode.BadRequest,
315-
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
352+
message: $"Could not match order by column {exposedOrderByFieldName} with a column in the pagination token with the same name and direction.",
353+
statusCode: HttpStatusCode.BadRequest,
354+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
316355
}
317356

318357
orderByColumnCount++;
319358
}
320359

321360
// the check above validates that all orderby columns are matched with after columns
322361
// also validate that there are no extra after columns
323-
if (afterDict.Count != orderByColumnCount)
362+
if (exposedFieldNameToBackingColumn.Count != orderByColumnCount)
324363
{
325364
throw new ArgumentException("After token contains extra columns not present in order by columns.");
326365
}
@@ -351,7 +390,7 @@ e is NotSupportedException
351390
innerException: e);
352391
}
353392

354-
return after;
393+
return paginationCursorColumnsForQuery;
355394
}
356395

357396
/// <summary>

src/Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
88
<PackageVersion Include="coverlet.msbuild" Version="3.2.0" />
99
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
10-
<PackageVersion Include="HotChocolate" Version="12.18.0" />
11-
<PackageVersion Include="HotChocolate.AspNetCore" Version="12.18.0" />
12-
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="12.18.0" />
13-
<PackageVersion Include="HotChocolate.Types.NodaTime" Version="12.18.0" />
10+
<PackageVersion Include="HotChocolate" Version="12.22.0" />
11+
<PackageVersion Include="HotChocolate.AspNetCore" Version="12.22.0" />
12+
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="12.22.0" />
13+
<PackageVersion Include="HotChocolate.Types.NodaTime" Version="12.22.0" />
1414
<PackageVersion Include="Humanizer" Version="2.14.1" />
1515
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
1616
<PackageVersion Include="DotNetEnv" Version="2.5.0" />

src/Service.Tests/Configuration/ConfigurationTests.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -988,10 +988,9 @@ public void TestConnectionStringEnvVarHasHighestPrecedence()
988988
}
989989
catch (Exception e)
990990
{
991-
Assert.AreEqual(typeof(ApplicationException), e.GetType());
991+
Assert.AreEqual(typeof(ArgumentException), e.GetType());
992992
Assert.AreEqual(
993-
$"Could not initialize the engine with the runtime config file: " +
994-
$"{CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{CONFIG_EXTENSION}",
993+
$"Format of the initialization string does not conform to specification starting at index 0.",
995994
e.Message);
996995
}
997996
}
@@ -1033,15 +1032,14 @@ public void TestGetConfigFileNameForEnvironment(
10331032
[TestCategory(TestCategory.MSSQL)]
10341033
[DataRow("/graphql/", HostMode.Development, HttpStatusCode.OK, "Banana Cake Pop",
10351034
DisplayName = "GraphQL endpoint with no query in development mode.")]
1036-
[DataRow("/graphql", HostMode.Production, HttpStatusCode.BadRequest,
1037-
"Either the parameter query or the parameter id has to be set",
1035+
[DataRow("/graphql", HostMode.Production, HttpStatusCode.NotFound,
10381036
DisplayName = "GraphQL endpoint with no query in production mode.")]
10391037
[DataRow("/graphql/ui", HostMode.Development, HttpStatusCode.NotFound,
10401038
DisplayName = "Default BananaCakePop in development mode.")]
10411039
[DataRow("/graphql/ui", HostMode.Production, HttpStatusCode.NotFound,
10421040
DisplayName = "Default BananaCakePop in production mode.")]
10431041
[DataRow("/graphql?query={book_by_pk(id: 1){title}}",
1044-
HostMode.Development, HttpStatusCode.Moved,
1042+
HostMode.Development, HttpStatusCode.OK,
10451043
DisplayName = "GraphQL endpoint with query in development mode.")]
10461044
[DataRow("/graphql?query={book_by_pk(id: 1){title}}",
10471045
HostMode.Production, HttpStatusCode.OK, "data",

src/Service.Tests/Configuration/TelemetryTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
using System.Net.Http.Json;
99
using System.Threading.Tasks;
1010
using Azure.DataApiBuilder.Config.ObjectModel;
11+
using Microsoft.ApplicationInsights;
1112
using Microsoft.ApplicationInsights.Channel;
1213
using Microsoft.ApplicationInsights.DataContracts;
1314
using Microsoft.AspNetCore.TestHost;
15+
using Microsoft.Extensions.DependencyInjection;
1416
using Microsoft.IdentityModel.Tokens;
1517
using Microsoft.VisualStudio.TestTools.UnitTesting;
1618
using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationTests;
@@ -89,6 +91,7 @@ public async Task TestTelemetryItemsAreSentCorrectly_NonHostedScenario()
8991
using (TestServer server = new(Program.CreateWebHostBuilder(args)))
9092
{
9193
await TestRestAndGraphQLRequestsOnServerInNonHostedScenario(server);
94+
Assert.IsTrue(server.Services.GetService<TelemetryClient>() is not null);
9295
}
9396

9497
List<ITelemetry> telemetryItems = ((CustomTelemetryChannel)telemetryChannel).GetTelemetryItems();
@@ -157,6 +160,9 @@ public async Task TestNoTelemetryItemsSentWhenDisabled_NonHostedScenario(bool is
157160
using (TestServer server = new(Program.CreateWebHostBuilder(args)))
158161
{
159162
await TestRestAndGraphQLRequestsOnServerInNonHostedScenario(server);
163+
// Telemetry client should be null if telemetry is disabled
164+
// Using an EXOR here to assert this.
165+
Assert.IsTrue(server.Services.GetService<TelemetryClient>() is null ^ isTelemetryEnabled);
160166
}
161167

162168
List<ITelemetry> telemetryItems = ((CustomTelemetryChannel)telemetryChannel).GetTelemetryItems();

src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1188,7 +1188,7 @@ public virtual async Task UpdateWithInvalidIdentifier()
11881188
/// Test adding a website placement to a book which already has a website
11891189
/// placement
11901190
/// </summary>
1191-
public async Task TestViolatingOneToOneRelashionShip(string errorMessage)
1191+
public async Task TestViolatingOneToOneRelationship(string errorMessage)
11921192
{
11931193
string graphQLMutationName = "createBookWebsitePlacement";
11941194
string graphQLMutation = @"

0 commit comments

Comments
 (0)