Skip to content
This repository has been archived by the owner on Aug 2, 2024. It is now read-only.

Commit

Permalink
(#783) Added includeDeleted to GetItemAsync (#790)
Browse files Browse the repository at this point in the history
* (#783) Added _includedeleted support to server.

* (#783) client side of GetItemAsync for includeDeleted
  • Loading branch information
adrianhall authored Oct 13, 2023
1 parent 2a68a40 commit 940cfa1
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,34 @@ internal static IQueryable<T> ApplyDataView<T>(this IQueryable<T> query, Express
/// <returns></returns>
internal static IQueryable<T> ApplyDeletedView<T>(this IQueryable<T> query, HttpRequest request, bool softDelete) where T : ITableData
{
if (!softDelete)
if (!softDelete || request.ShouldIncludeDeletedItems())
{
return query;
}
return query.Where(m => !m.Deleted);
}

// Query string options: __includedeleted=true
/// <summary>
/// Determines if a request should include deleted items.
/// </summary>
/// <param name="request">The request</param>
/// <param name="softDelete"><c>true</c> if soft delete is enabled</param>
/// <returns><c>true</c> if the request should take into consideration deleted items.</returns>
internal static bool ShouldIncludeDeletedItems(this HttpRequest request)
{
// Query option: ?__includedeleted=true
if (request.Query.ContainsKey(IncludeDeletedParameter) && request.Query[IncludeDeletedParameter][0].Equals("true", StringComparison.InvariantCultureIgnoreCase))
{
return query;
return true;
}

// Header option: X-ZUMO-Options: __includedeleted
if (request.Headers.ContainsKey(ZumoOptionsHeader) && request.Headers[ZumoOptionsHeader].Contains(IncludeDeletedOption, StringComparer.InvariantCultureIgnoreCase))
{
return query;
return true;
}

return query.Where(m => !m.Deleted);
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ public virtual async Task<IActionResult> ReadAsync([FromRoute] string id, Cancel
return NotFound();
}
await AuthorizeRequest(TableOperation.Read, entity, token).ConfigureAwait(false);
if (Options.EnableSoftDelete && entity.Deleted)
if (Options.EnableSoftDelete && entity.Deleted && !Request.ShouldIncludeDeletedItems())
{
Logger?.LogWarning("Read({Id}): Item not found (soft-delete)", id);
return StatusCode(StatusCodes.Status410Gone);
Expand Down
18 changes: 18 additions & 0 deletions sdk/dotnet/src/Microsoft.Datasync.Client/Table/IRemoteTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ public interface IReadOnlyRemoteTable
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>A task that returns the item when complete.</returns>
Task<JToken> GetItemAsync(string id, CancellationToken cancellationToken = default);

/// <summary>
/// Retrieve an item from the remote table.
/// </summary>
/// <param name="id">The ID of the item to retrieve.</param>
/// <param name="includeDeleted">If <c>true</c>, a soft-deleted item will be returned; if <c>false</c>, GONE is returned.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>A task that returns the item when complete.</returns>
Task<JToken> GetItemAsync(string id, bool includeDeleted, CancellationToken cancellationToken = default);
}

/// <summary>
Expand Down Expand Up @@ -143,6 +152,15 @@ public interface IReadOnlyRemoteTable<T> : IReadOnlyRemoteTable, ILinqMethods<T>
/// <returns>A task that returns the item when complete.</returns>
new Task<T> GetItemAsync(string id, CancellationToken cancellationToken = default);

/// <summary>
/// Retrieve an item from the remote table.
/// </summary>
/// <param name="id">The ID of the item to retrieve.</param>
/// <param name="includeDeleted">If <c>true</c>, a soft-deleted item will be returned; if <c>false</c>, GONE is returned.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>A task that returns the item when complete.</returns>
new Task<T> GetItemAsync(string id, bool includeDeleted, CancellationToken cancellationToken = default);

/// <summary>
/// Refreshes the current instance with the latest values from the table.
/// </summary>
Expand Down
13 changes: 12 additions & 1 deletion sdk/dotnet/src/Microsoft.Datasync.Client/Table/RemoteTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,23 @@ public IAsyncEnumerable<JToken> GetAsyncItems(string query)
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>A task that returns the item when complete.</returns>
public Task<JToken> GetItemAsync(string id, CancellationToken cancellationToken = default)
=> GetItemAsync(id, false, cancellationToken);

/// <summary>
/// Retrieve an item from the remote table.
/// </summary>
/// <param name="id">The ID of the item to retrieve.</param>
/// <param name="includeDeleted">If <c>true</c>, a soft-deleted item will be returned; if <c>false</c>, GONE is returned.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>A task that returns the item when complete.</returns>
public Task<JToken> GetItemAsync(string id, bool includeDeleted, CancellationToken cancellationToken = default)
{
Arguments.IsValidId(id, nameof(id));
string query = includeDeleted ? "?__includedeleted=true" : string.Empty;
ServiceRequest request = new()
{
Method = HttpMethod.Get,
UriPathAndQuery = $"{TableEndpoint}/{id}",
UriPathAndQuery = $"{TableEndpoint}/{id}{query}",
EnsureResponseContent = true
};
return SendRequestAsync(request, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,20 @@ public IAsyncEnumerable<U> GetAsyncItems<U>(ITableQuery<U> query)
/// <param name="id">The ID of the item to retrieve.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>A task that returns the item when complete.</returns>
public new async Task<T> GetItemAsync(string id, CancellationToken cancellationToken = default)
public new Task<T> GetItemAsync(string id, CancellationToken cancellationToken = default)
=> GetItemAsync(id, false, cancellationToken);


/// <summary>
/// Retrieve an item from the remote table.
/// </summary>
/// <param name="id">The ID of the item to retrieve.</param>
/// <param name="includeDeleted">If <c>true</c>, a soft-deleted item will be returned; if <c>false</c>, GONE is returned.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
/// <returns>A task that returns the item when complete.</returns>
public new async Task<T> GetItemAsync(string id, bool includeDeleted, CancellationToken cancellationToken = default)
{
JToken value = await base.GetItemAsync(id, cancellationToken).ConfigureAwait(false);
JToken value = await base.GetItemAsync(id, includeDeleted, cancellationToken).ConfigureAwait(false);
return ServiceClient.Serializer.Deserialize<T>(value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Microsoft.AspNetCore.Datasync.Swashbuckle.Test;

[ExcludeFromCodeCoverage]
public class SwaggerGen_Tests
{
private readonly TestServer server = SwaggerServer.CreateTestServer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ public async Task GetItemAsync_FormulatesCorrectRequest()
AssertJsonMatches(response);
}

[Fact]
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_FormulatesCorrectRequest_IncludeDeleted()
{
// Arrange
MockHandler.AddResponse(HttpStatusCode.OK, payload);

// Act
var response = await table.GetItemAsync(sId, true).ConfigureAwait(false);

// Assert
var request = AssertSingleRequest(HttpMethod.Get, expectedEndpoint + "?__includedeleted=true");
Assert.False(request.Headers.Contains("If-None-Match"));
AssertJsonMatches(response);
}

[Fact]
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_FormulatesCorrectRequest_WithAuth()
Expand Down Expand Up @@ -88,7 +104,7 @@ public async Task GetItemAsync_RequestFailed_Throws(HttpStatusCode statusCode)

[Fact]
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_Fails_OnBdJson()
public async Task GetItemAsync_Fails_OnBadJson()
{
// Arrange
ReturnBadJson(HttpStatusCode.OK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,20 @@ public async Task CountItemsAsync_Count()
_ = AssertSingleRequest(HttpMethod.Get, tableEndpoint + $"?{CountArgs}");
Assert.Equal(42, count);
}

[Fact]
[Trait("Method", "LongCountAsync")]
public async Task LongCountAsync_Count()
{
// Arrange
CreatePageOfJsonItems(1, 42);

// Act
var count = await table.LongCountAsync();

// Assert
_ = AssertSingleRequest(HttpMethod.Get, tableEndpoint + $"?{CountArgs}");
Assert.Equal(42, count);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ public async Task GetItemAsync_FormulatesCorrectRequest()
Assert.Equal(payload, actual);
}

[Fact]
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_FormulatesCorrectRequest_IncludeDeleted()
{
// Arrange
MockHandler.AddResponse(HttpStatusCode.OK, payload);

// Act
var actual = await table.GetItemAsync(sId, true).ConfigureAwait(false);

// Assert
AssertSingleRequest(HttpMethod.Get, expectedEndpoint + "?__includedeleted=true");
Assert.Equal(payload, actual);
}

[Fact]
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_FormulatesCorrectRequest_WithAuth()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ public async Task GetItemAsync_GetIfNotSoftDeleted()
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_GoneIfSoftDeleted()
{
// Arrange
// Arrange
var id = GetRandomId();
await MovieServer.SoftDeleteMoviesAsync(x => x.Id == id).ConfigureAwait(false);
Expand All @@ -69,4 +68,20 @@ public async Task GetItemAsync_GoneIfSoftDeleted()
Assert.NotNull(exception.Response);
Assert.Equal(HttpStatusCode.Gone, exception.Response?.StatusCode);
}

[Fact]
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_GetIfSoftDeleted()
{
// Arrange
var id = GetRandomId();
await MovieServer.SoftDeleteMoviesAsync(x => x.Id == id).ConfigureAwait(false);
var expected = MovieServer.GetMovieById(id)!;

// Act
var response = await soft.GetItemAsync(id, true).ConfigureAwait(false);

// Assert
AssertJsonDocumentMatches(expected, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,21 @@ public async Task GetItemAsync_GoneIfSoftDeleted()
Assert.NotNull(exception.Response);
Assert.Equal(HttpStatusCode.Gone, exception.Response?.StatusCode);
}

[Fact]
[Trait("Method", "GetItemAsync")]
public async Task GetItemAsync_GetIfSoftDeleted()
{
// Arrange
var id = GetRandomId();
await MovieServer.SoftDeleteMoviesAsync(x => x.Id == id).ConfigureAwait(false);
var expected = MovieServer.GetMovieById(id)!;

// Act
var response = await soft.GetItemAsync(id, true).ConfigureAwait(false);

// Assert
AssertEx.SystemPropertiesMatch(expected, response);
Assert.Equal<IMovie>(expected, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,19 @@ public async Task ReadSoftDeletedItem_ReturnsGoneIfDeleted([CombinatorialValues(
var response = await MovieServer.SendRequest(HttpMethod.Get, $"tables/{table}/{id}");
await AssertResponseWithLoggingAsync(HttpStatusCode.Gone, response);
}

[Theory, CombinatorialData]
public async Task ReadSoftDeletedItem_ReturnsIfDeletedItemsIncluded([CombinatorialValues("soft", "soft_logged")] string table)
{
var id = GetRandomId();
await MovieServer.SoftDeleteMoviesAsync(x => x.Id == id);
var expected = MovieServer.GetMovieById(id)!;

var response = await MovieServer.SendRequest(HttpMethod.Get, $"tables/{table}/{id}?__includedeleted=true");
await AssertResponseWithLoggingAsync(HttpStatusCode.OK, response);
var actual = response.DeserializeContent<ClientMovie>();
Assert.Equal<IMovie>(expected, actual!);
AssertEx.SystemPropertiesMatch(expected, actual);
AssertEx.ResponseHasConditionalHeaders(expected, response);
}
}

0 comments on commit 940cfa1

Please sign in to comment.