Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Made patch requests partially strongly typed #53

Merged
merged 1 commit into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions back-end/Database/Infrastructure/AsyncTenantedDocumentSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,19 @@ private async Task<bool> TenantedEntityExistsAsync<TEntity>(string fullId, Cance

#region Processing Deferred Path Queries [PUBLIC, PRIVATE] ------------

/// <inheritdoc/>
public IndexQuery GetIndexQuery<T>(IRavenQueryable<T> queryable)
{
var inspector = (IRavenQueryInspector)queryable;
return inspector.GetIndexQuery(isAsync: true);
}

/// <inheritdoc/>
public void AddDeferredPatchQuery(IndexQuery patchQuery)
{
var match = Regex.Match(patchQuery.Query, @$"\b{nameof(ITenantedEntity.TenantId)}\b", RegexOptions.IgnoreCase);
if (match.Success)
throw new ArgumentException("Attempt to access a tenant in RQL");
// Consider adding tenant validation in here
// E.g. by running Regex.Match(patchQuery.Query, @$"\b{nameof(ITenantedEntity.TenantId)}\b", RegexOptions.IgnoreCase);

_deferredPatchQueries.Enqueue(patchQuery);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,15 @@ public interface IAsyncTenantedDocumentSession : IDisposable
#endregion / IAsyncDocumentSession methods ----------------------------

#region Patch requests (aka Set based operations) ---------------------


/// <summary>
/// Get <see cref="IndexQuery"/> for a strongly-typed query
/// </summary>
/// <remarks>
/// Leverage a strongly-typed WHERE condition while the UPDATE section is a JavaScript string (https://github.com/ravendb/ravendb/issues/12650)
/// </remarks>
IndexQuery GetIndexQuery<T>(IRavenQueryable<T> queryable);

/// <summary>
/// Add a RavenDB patch request for executing after calling <see cref="IAsyncDocumentSession.SaveChangesAsync"/>
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System.Text.RegularExpressions;
using Raven.Client.Documents.Linq;

using Raven.Client;
using Raven.Client.Documents.Queries;
using Raven.Yabt.Database.Infrastructure;
using Raven.Yabt.Database.Models.BacklogItems;
using Raven.Yabt.Database.Models.BacklogItems.Indexes;
using Raven.Yabt.Domain.Common;
using Raven.Yabt.Domain.CustomFieldServices.Command;
using Raven.Yabt.Domain.Helpers;

namespace Raven.Yabt.Domain.BacklogItemServices.Commands;

Expand All @@ -19,30 +18,27 @@ public void ClearCustomFieldId(string customFieldId)
if (string.IsNullOrEmpty(customFieldId))
return;

var sanitisedId = GetSanitisedId(customFieldId).ToUpper();
var sanitisedId = customFieldId.GetSanitisedIdForPatchQuery();

// Form a patch query
var queryString= $@"FROM INDEX '{new BacklogItems_ForList().IndexName}' AS i
WHERE i.{nameof(BacklogItem.CustomFields)}_{sanitisedId} != null
UPDATE
{{
delete i.{nameof(BacklogItem.CustomFields)}[$id];
}}";
var query = new IndexQuery
{
Query = queryString,
QueryParameters = new Parameters
{
{ "id", sanitisedId }
}
};

// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
#pragma warning disable CS8073
var idxQuery = DbSession.GetIndexQuery(
DbSession.Query<BacklogItemIndexedForList, BacklogItems_ForList>()
.Where(i => i.CustomFields![sanitisedId] != null)
);
#pragma warning restore CS8073

idxQuery.Query += $@"UPDATE
{{
delete this.{nameof(BacklogItem.CustomFields)}[$id];
}}";
// Append parameters rather than overwriting, as it already has some from the strongly-typed WHERE condition
idxQuery.QueryParameters.Add("id", sanitisedId);


// Add the patch to a collection
DbSession.AddDeferredPatchQuery(query);
DbSession.AddDeferredPatchQuery(idxQuery);
}

/// <summary>
/// Replace invalid characters with empty strings. Can't pass it as a parameter, as string parameters get wrapped in '\"' when inserted
/// </summary>
private static string GetSanitisedId(string id) => Regex.Replace(id, @"[^\w\.@-]", "");
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System.Text.RegularExpressions;
using Raven.Client.Documents.Linq;

using Raven.Client;
using Raven.Client.Documents.Queries;
using Raven.Yabt.Database.Common.BacklogItem;
using Raven.Yabt.Database.Common.References;
using Raven.Yabt.Database.Infrastructure;
using Raven.Yabt.Database.Models.BacklogItems;
using Raven.Yabt.Database.Models.BacklogItems.Indexes;
using Raven.Yabt.Domain.Common;
using Raven.Yabt.Domain.Helpers;
using Raven.Yabt.Domain.UserServices.Command;

namespace Raven.Yabt.Domain.BacklogItemServices.Commands;
Expand All @@ -16,85 +15,63 @@ internal class UpdateUserReferencesCommand : BaseDbService, IUpdateUserReference
{
public UpdateUserReferencesCommand(IAsyncTenantedDocumentSession session): base(session) {}

/// <inheritdoc/>
public void ClearUserId(string userId)
{
if (string.IsNullOrEmpty(userId))
return;

var sanitisedId = GetSanitisedId(userId);

// Form a patch query
var queryString= $@"FROM INDEX '{new BacklogItems_ForList().IndexName}' AS i
WHERE i.{nameof(BacklogItemIndexedForList.ModifiedBy)}_{sanitisedId} != null OR i.{nameof(BacklogItemIndexedForList.AssignedUserId)} == $userId
UPDATE
{{
if (i.{nameof(BacklogItemIndexedForList.Assignee)}.{nameof(UserReference.Id)}.toUpperCase() == $userId) {{
i.{nameof(BacklogItemIndexedForList.Assignee)} = null;
}}
i.{nameof(BacklogItem.Comments)}.forEach(comment => {{
if (comment.{nameof(Comment.Author)}.{nameof(UserReference.Id)}.toUpperCase() == $userId) {{
comment.{nameof(Comment.Author)}.{nameof(UserReference.Id)} = null;
}}
}});
i.{nameof(BacklogItem.ModifiedBy)}.forEach(modif => {{
if (modif.{nameof(BacklogItemHistoryRecord.ActionedBy)}.{nameof(UserReference.Id)}.toUpperCase() == $userId)
modif.{nameof(BacklogItemHistoryRecord.ActionedBy)}.{nameof(UserReference.Id)} = null;
}});
}}";
var query = new IndexQuery
{
Query = queryString,
QueryParameters = new Parameters
{
{ "userId", userId.ToUpper() }
}
};

// Add the patch to a collection
DbSession.AddDeferredPatchQuery(query);
}
=> UpdateUserReferenceInBacklogItems(userId, null);

/// <inheritdoc/>
public void UpdateReferences(UserReference newUserReference)
=> UpdateUserReferenceInBacklogItems(newUserReference.Id, newUserReference);

private void UpdateUserReferenceInBacklogItems(string? userId, UserReference? userReference)
{
if (string.IsNullOrEmpty(newUserReference.Id))
if (string.IsNullOrEmpty(userId))
return;

var sanitisedId = GetSanitisedId(newUserReference.Id);

var sanitisedId = userId.GetSanitisedIdForPatchQuery();
// Form a patch query
var queryString = $@"FROM INDEX '{new BacklogItems_ForList().IndexName}' AS i
WHERE i.{nameof(BacklogItemIndexedForList.ModifiedBy)}_{sanitisedId} != null OR i.{nameof(BacklogItemIndexedForList.AssignedUserId)} == $userId
UPDATE
{{
if (i.{nameof(BacklogItemIndexedForList.Assignee)}.{nameof(UserReference.Id)}.toUpperCase() == $userId) {{
i.{nameof(BacklogItemIndexedForList.Assignee)} = $userRef;
}}
i.{nameof(BacklogItem.Comments)}.forEach(comment => {{
if (comment.{nameof(Comment.Author)}.{nameof(UserReference.Id)}.toUpperCase() == $userId) {{
comment.{nameof(Comment.Author)} = $userRef;
}}
}});
i.{nameof(BacklogItem.ModifiedBy)}.forEach(modif => {{
if (modif.{nameof(BacklogItemHistoryRecord.ActionedBy)}.{nameof(UserReference.Id)}.toUpperCase() == $userId)
modif.{nameof(BacklogItemHistoryRecord.ActionedBy)} = $userRef;
}});
}}";
var query = new IndexQuery
{
Query = queryString,
QueryParameters = new Parameters
{
{ "userId", GetSanitisedId(newUserReference.Id).ToUpper() },
{ "userRef", newUserReference },
}
};

// ReSharper disable once ConditionIsAlwaysTrueOrFalse
#pragma warning disable CS8073
var idxQuery = DbSession.GetIndexQuery(
DbSession.Query<BacklogItemIndexedForList, BacklogItems_ForList>()
.Where(i
=> i.ModifiedByUser[sanitisedId] != null // Any modification done by the user (e.g. a comment or an update)
|| i.AssignedUserId == sanitisedId // The assignee of the ticket
)
);
#pragma warning restore CS8073

idxQuery.Query += $@" UPDATE
{{
if (this.{nameof(BacklogItemIndexedForList.Assignee)}.{nameof(UserReference.Id)}.toUpperCase() == $userId)
this.{nameof(BacklogItemIndexedForList.Assignee)} = $userRef;

this.{nameof(BacklogItem.Comments)}.forEach(comment => {{
if (comment.{nameof(Comment.Author)}.{nameof(UserReference.Id)}.toUpperCase() == $userId) {{
if ($userRef == null)
// Remove the user's ID in the reference but keep the old name
comment.{nameof(Comment.Author)}.{nameof(UserReference.Id)} = null;
else
comment.{nameof(Comment.Author)} = $userRef;
}}
}});
this.{nameof(BacklogItem.ModifiedBy)}.forEach(modif => {{
if (modif.{nameof(BacklogItemHistoryRecord.ActionedBy)}.{nameof(UserReference.Id)}.toUpperCase() == $userId) {{
if ($userRef == null)
// Remove the user's ID in the reference but keep the old name
modif.{nameof(BacklogItemHistoryRecord.ActionedBy)}.{nameof(UserReference.Id)} = null;
else
modif.{nameof(BacklogItemHistoryRecord.ActionedBy)} = $userRef;
}}
}});
}}";
// Append parameters rather than overwriting, as it already has some from the strongly-typed WHERE condition
idxQuery.QueryParameters.Add("userId", sanitisedId);
idxQuery.QueryParameters.Add("userRef", userReference);

// Add the patch to a collection
DbSession.AddDeferredPatchQuery(query);
DbSession.AddDeferredPatchQuery(idxQuery);
}

/// <summary>
/// Replace invalid characters with empty strings. Can't pass it as a parameter, as string parameters get wrapped in '\"' when inserted
/// </summary>
private static string GetSanitisedId(string id) => Regex.Replace(id, @"[^\w\.@-]", "");
}
Loading