Skip to content

Commit

Permalink
Merge pull request #3523 from signalco-io/feat/api-contact-delete-end…
Browse files Browse the repository at this point in the history
…point

Feat/api contact delete endpoint
  • Loading branch information
AleksandarDev authored Sep 29, 2023
2 parents 8c66b11 + cc7f0dd commit 465cc1f
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 75 deletions.
15 changes: 15 additions & 0 deletions cloud/src/Signal.Api.Common/Contact/EntityContactSetDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;

namespace Signal.Api.Common.Contact;

[Serializable]
public class EntityContactSetDto
{

[JsonPropertyName("valueSerialized")]
public string? ValueSerialized { get; set; }

[JsonPropertyName("timeStamp")]
public DateTime? TimeStamp { get; set; }
}
61 changes: 45 additions & 16 deletions cloud/src/Signal.Api.Common/Exceptions/HttpRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,16 @@ await req.JsonResponseAsync(
ex.Code,
cancellationToken: cancellationToken);

public static async Task<HttpResponseData> UserRequest<TResponse>(
this HttpRequestData req,
CancellationToken cancellationToken,
IFunctionAuthenticator authenticator,
Func<UserRequestContext, Task<TResponse>> executionBody)
{
try
{
var user = await authenticator.AuthenticateAsync(req, cancellationToken);
return await req.JsonResponseAsync(await executionBody(new UserRequestContext(user, cancellationToken)), cancellationToken: cancellationToken);
}
catch (ExpectedHttpException ex)
{
return await ExceptionResponseAsync(req, ex, cancellationToken);
}
}
//public static Task<HttpResponseData> UserRequest(
// this HttpRequestData req,
// CancellationToken cancellationToken,
// IFunctionAuthenticator authenticator,
// Func<UserRequestContext, Task> executionBody) =>
// UserRequest(req, authenticator, async context =>
// {
// await executionBody(context);
// return req.CreateResponse(HttpStatusCode.OK);
// }, cancellationToken);

public static Task<HttpResponseData> UserRequest<TPayload>(
this HttpRequestData req,
Expand Down Expand Up @@ -177,6 +171,41 @@ private static async Task<HttpResponseData> UserOrSystemRequest<TPayload>(
}
}

public static async Task<HttpResponseData> UserRequest(
this HttpRequestData req,
CancellationToken cancellationToken,
IFunctionAuthenticator authenticator,
Func<UserRequestContext, Task> executionBody)
{
try
{
var user = await authenticator.AuthenticateAsync(req, cancellationToken);
await executionBody(new UserRequestContext(user, cancellationToken));
return req.CreateResponse(HttpStatusCode.OK);
}
catch (ExpectedHttpException ex)
{
return await ExceptionResponseAsync(req, ex, cancellationToken);
}
}

public static async Task<HttpResponseData> UserRequest<TResponse>(
this HttpRequestData req,
CancellationToken cancellationToken,
IFunctionAuthenticator authenticator,
Func<UserRequestContext, Task<TResponse>> executionBody)
{
try
{
var user = await authenticator.AuthenticateAsync(req, cancellationToken);
return await req.JsonResponseAsync(await executionBody(new UserRequestContext(user, cancellationToken)), cancellationToken: cancellationToken);
}
catch (ExpectedHttpException ex)
{
return await ExceptionResponseAsync(req, ex, cancellationToken);
}
}

private static async Task<HttpResponseData> UserRequest<TPayload>(
this HttpRequestData req,
IFunctionAuthenticator authenticator,
Expand Down
111 changes: 74 additions & 37 deletions cloud/src/Signal.Core/Entities/EntityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,24 @@

namespace Signal.Core.Entities;

internal class EntityService : IEntityService
internal class EntityService(
ISharingService sharingService,
IAzureStorageDao storageDao,
IAzureStorage storage,
IProcessManager processManager,
ISignalRService signalRService) : IEntityService
{
private readonly ISharingService sharingService;
private readonly IAzureStorageDao storageDao;
private readonly IAzureStorage storage;
private readonly IProcessManager processManager;
private readonly ISignalRService signalRService;

public EntityService(
ISharingService sharingService,
IAzureStorageDao storageDao,
IAzureStorage storage,
IProcessManager processManager,
ISignalRService signalRService)
{
this.sharingService = sharingService ?? throw new ArgumentNullException(nameof(sharingService));
this.storageDao = storageDao ?? throw new ArgumentNullException(nameof(storageDao));
this.storage = storage ?? throw new ArgumentNullException(nameof(storage));
this.processManager = processManager ?? throw new ArgumentNullException(nameof(processManager));
this.signalRService = signalRService ?? throw new ArgumentNullException(nameof(signalRService));
}
private readonly ISharingService sharingService =
sharingService ?? throw new ArgumentNullException(nameof(sharingService));

private readonly IAzureStorageDao storageDao = storageDao ?? throw new ArgumentNullException(nameof(storageDao));
private readonly IAzureStorage storage = storage ?? throw new ArgumentNullException(nameof(storage));

private readonly IProcessManager processManager =
processManager ?? throw new ArgumentNullException(nameof(processManager));

private readonly ISignalRService signalRService =
signalRService ?? throw new ArgumentNullException(nameof(signalRService));

public async Task<IReadOnlyDictionary<string, IEnumerable<string>>> EntityUsersAsync(
IEnumerable<string> entityIds,
Expand All @@ -45,18 +42,19 @@ await this.storageDao.AssignedUsersAsync(
cancellationToken);

public async Task<IEnumerable<IEntity>> AllAsync(
string userId,
IEnumerable<EntityType>? types = null,
string userId,
IEnumerable<EntityType>? types = null,
CancellationToken cancellationToken = default) =>
await this.storageDao.UserEntitiesAsync(userId, types, cancellationToken);

public async Task<IEnumerable<IEntityDetailed>> AllDetailedAsync(
string userId,
IEnumerable<EntityType>? types = null,
string userId,
IEnumerable<EntityType>? types = null,
CancellationToken cancellationToken = default) =>
await this.storageDao.UserEntitiesDetailedAsync(userId, types, cancellationToken);

public async Task<IEntityDetailed?> GetDetailedAsync(string userId, string entityId, CancellationToken cancellationToken = default)
public async Task<IEntityDetailed?> GetDetailedAsync(string userId, string entityId,
CancellationToken cancellationToken = default)
{
var assignedTask = this.IsUserAssignedAsync(userId, entityId, cancellationToken);
var getTask = this.storageDao.GetDetailedAsync(entityId, cancellationToken);
Expand All @@ -65,10 +63,11 @@ public async Task<IEnumerable<IEntityDetailed>> AllDetailedAsync(
return !assignedTask.Result ? null : getTask.Result;
}

public async Task<IEntity?> GetInternalAsync(string entityId, CancellationToken cancellationToken = default) =>
public async Task<IEntity?> GetInternalAsync(string entityId, CancellationToken cancellationToken = default) =>
await this.storageDao.GetAsync(entityId, cancellationToken);

public async Task<string> UpsertAsync(string userId, string? entityId, Func<string, IEntity> entityFunc, CancellationToken cancellationToken = default)
public async Task<string> UpsertAsync(string userId, string? entityId, Func<string, IEntity> entityFunc,
CancellationToken cancellationToken = default)
{
// Check if existing entity was requested but not assigned
var exists = false;
Expand Down Expand Up @@ -100,18 +99,20 @@ await this.sharingService.AssignToUserAsync(
return id;
}

public async Task<IContact?> ContactAsync(IContactPointer pointer, CancellationToken cancellationToken = default) =>
public async Task<IContact?> ContactAsync(IContactPointer pointer, CancellationToken cancellationToken = default) =>
await this.storageDao.ContactAsync(pointer, cancellationToken);

public async Task<IEnumerable<IContact>?> ContactsAsync(string entityId, CancellationToken cancellationToken = default) =>
public async Task<IEnumerable<IContact>?> ContactsAsync(string entityId,
CancellationToken cancellationToken = default) =>
await this.storageDao.ContactsAsync(entityId, cancellationToken);

public async Task<IEnumerable<IContact?>> ContactsAsync(
IEnumerable<IContactPointer> pointers,
IEnumerable<IContactPointer> pointers,
CancellationToken cancellationToken = default) =>
await Task.WhenAll(pointers.Select(p => this.ContactAsync(p, cancellationToken)));

public async Task<IEnumerable<IContact>?> ContactsAsync(IEnumerable<string> entityIds, CancellationToken cancellationToken = default) =>
public async Task<IEnumerable<IContact>?> ContactsAsync(IEnumerable<string> entityIds,
CancellationToken cancellationToken = default) =>
await this.storageDao.ContactsAsync(entityIds, cancellationToken);

public async Task ContactSetMetadataAsync(
Expand Down Expand Up @@ -202,7 +203,7 @@ await Task.WhenAll(

// Processing
var queueStateProcessingTask = Task.CompletedTask;
if (!doNotProcess)
if (!doNotProcess)
queueStateProcessingTask = this.processManager.AddAsync(pointer, cancellationToken);

// Caching
Expand All @@ -220,6 +221,23 @@ await Task.WhenAll(
}
}

public async Task ContactDeleteAsync(
string userId,
IContactPointer pointer,
CancellationToken cancellationToken = default)
{
// Validate assignment
if (!(await this.IsUserAssignedAsync(userId, pointer.EntityId, cancellationToken)))
throw new ExpectedHttpException(HttpStatusCode.NotFound);

// TODO: Check if user is owner

// TODO: Delete contact history

// Delete contact
await this.storage.RemoveAsync(pointer, cancellationToken);
}

private async Task CacheEntityAsync(IContactPointer pointer, CancellationToken cancellationToken = default)
{
// NOTE: Implementation checks whether given contact is appropriate to be cached
Expand All @@ -241,15 +259,34 @@ public async Task RemoveAsync(string userId, string entityId, CancellationToken
throw new ExpectedHttpException(HttpStatusCode.NotFound);

// TODO: Check if user is owner (only owner can delete entity)
// TODO: Remove assignments for all users (since entity doesn't exist anymore)
// TODO: Remove all contact history
// TODO: Remove all contact

// Remove assignments for all users (since entity doesn't exist anymore)
var entityUsers = (await this.storageDao.AssignedUsersAsync(new[] {entityId}, cancellationToken))[entityId];
var assignmentsDeleteTask = Task.WhenAll(entityUsers.Select(u =>
this.sharingService.UnAssignFromUserAsync(u, entityId, cancellationToken)));

// Remove all contacts
Task? contactsDeleteTask = null;
var contacts = await this.storageDao.ContactsAsync(entityId, cancellationToken);
if (contacts != null)
{
contactsDeleteTask = Task.WhenAll(contacts.Select(c =>
this.ContactDeleteAsync(
userId,
new ContactPointer(entityId, c.ChannelName, c.ContactName),
cancellationToken)));
}

// Wait for all to finish
await Task.WhenAll(
contactsDeleteTask ?? Task.CompletedTask,
assignmentsDeleteTask);

// Remove entity
await this.storage.RemoveEntityAsync(entityId, cancellationToken);
}

public Task<bool> IsUserAssignedAsync(string userId, string id, CancellationToken cancellationToken = default) =>
public Task<bool> IsUserAssignedAsync(string userId, string id, CancellationToken cancellationToken = default) =>
this.storageDao.IsUserAssignedAsync(userId, id, cancellationToken);

public async Task BroadcastToEntityUsersAsync(
Expand All @@ -259,7 +296,7 @@ public async Task BroadcastToEntityUsersAsync(
object[] arguments,
CancellationToken cancellationToken = default)
{
var entityUsers = await this.EntityUsersAsync(new[] { entityId }, cancellationToken);
var entityUsers = await this.EntityUsersAsync(new[] {entityId}, cancellationToken);
await this.signalRService.SendToUsersAsync(
entityUsers[entityId].ToList(),
hubName,
Expand Down
5 changes: 5 additions & 0 deletions cloud/src/Signal.Core/Entities/IEntityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,9 @@ Task ContactSetMetadataAsync(
IContactPointer pointer,
string? metadata,
CancellationToken cancellationToken = default);

Task ContactDeleteAsync(
string userId,
IContactPointer pointer,
CancellationToken cancellationToken = default);
}
19 changes: 6 additions & 13 deletions cloud/src/Signal.Core/Sharing/SharingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,14 @@

namespace Signal.Core.Sharing;

public class SharingService : ISharingService
{
private readonly IAzureStorage azureStorage;
private readonly IUserService userService;
private readonly ILogger<SharingService> logger;

public SharingService(
IAzureStorage azureStorage,
public class SharingService(IAzureStorage azureStorage,
IUserService userService,
ILogger<SharingService> logger)
{
this.azureStorage = azureStorage ?? throw new ArgumentNullException(nameof(azureStorage));
this.userService = userService ?? throw new ArgumentNullException(nameof(userService));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
: ISharingService
{
private readonly IAzureStorage azureStorage = azureStorage;
private readonly IUserService userService = userService;
private readonly ILogger<SharingService> logger = logger;

public async Task AssignToUserEmailAsync(
string userEmail,
Expand Down
1 change: 1 addition & 0 deletions cloud/src/Signal.Core/Storage/IAzureStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public interface IAzureStorage
Task QueueAsync(ContactStateProcessQueueItem item, CancellationToken cancellationToken = default);
Task QueueAsync(UsageQueueItem? item, CancellationToken cancellationToken = default);
Task UpsertAsync(IContactLinkProcessTriggerItem cacheItem, CancellationToken cancellationToken = default);
Task RemoveAsync(IContactPointer contactPointer, CancellationToken cancellationToken);
Task RemoveAsync(IUserAssignedEntity assignment, CancellationToken cancellationToken = default);
Task EnsureTableAsync(string tableName, CancellationToken cancellationToken = default);
Task EnsureQueueAsync(string queueName, CancellationToken cancellationToken = default);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal class AzureContact : AzureTableEntityBase
public AzureContact() : base(string.Empty, string.Empty)
{
}

protected AzureContact(string partitionKey, string rowKey) : base(partitionKey, rowKey)
{
}
Expand All @@ -33,9 +33,13 @@ public static AzureContact From(IContact contact)
};
}

public static (string partitionKey, string rowKey) ToStorageIdentifier(IContactPointer contactPointer) =>
(contactPointer.EntityId, $"{contactPointer.ChannelName}-{contactPointer.ContactName}");

public static AzureContact From(IContactPointer contactPointer)
{
return new AzureContact(contactPointer.EntityId, $"{contactPointer.ChannelName}-{contactPointer.ContactName}")
var (partitionKey, rowKey) = ToStorageIdentifier(contactPointer);
return new AzureContact(partitionKey, rowKey)
{
Name = contactPointer.ContactName,
TimeStamp = DateTime.UtcNow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ await this.WithQueueClientAsync(
cancellationToken: cancellationToken), cancellationToken);
}

public Task RemoveAsync(IContactPointer contactPointer, CancellationToken cancellationToken)
{
var (partitionKey, rowKey) = AzureContact.ToStorageIdentifier(contactPointer);
return this.WithClientAsync(
ItemTableNames.Contacts,
c => c.DeleteEntityAsync(partitionKey, rowKey, cancellationToken: cancellationToken), cancellationToken);
}

public async Task RemoveAsync(IUserAssignedEntity assignment, CancellationToken cancellationToken = default) =>
await this.WithClientAsync(
ItemTableNames.UserAssignedEntity,
Expand Down
Loading

0 comments on commit 465cc1f

Please sign in to comment.