Working on the workflow template manager

This commit is contained in:
Colin Dawson 2026-02-04 21:25:44 +00:00
parent 1a82958300
commit 7a6ff96ef8
25 changed files with 291 additions and 244 deletions

View File

@ -0,0 +1,6 @@
namespace e_suite.API.Common;
public interface IWorkflowTemplateManager
{
}

View File

@ -1,9 +1,76 @@
using e_suite.Database.Audit.Models;
using e_suite.Database.Audit.Tables.Audit;
using System.ComponentModel;
namespace e_suite.API.Common.models;
public class AuditLogEntry : IId
{
public AuditLogEntry()
{
}
public AuditLogEntry(AuditEntry auditEntry)
{
Id = auditEntry.Id;
UserId = auditEntry.AuditLog.UserId;
UserDisplayName = auditEntry.AuditLog.UserDisplayName;
Type = auditEntry.AuditLog.Type;
DateTime = auditEntry.AuditLog.DateTime;
Fields = auditEntry.AuditLog.Fields;
Comment = auditEntry.AuditLog.Comment;
EntityName = auditEntry.EntityName;
EntityDisplayName = GetDisplayNameForClass(auditEntry.EntityName);
PrimaryKey = auditEntry.PrimaryKey;
DisplayName = auditEntry.DisplayName ?? string.Empty;
}
private string GetDisplayNameForClass(string entityName)
{
var type = GetTypeFromFullName(entityName);
if (type != null)
{
var displayName = GetDisplayName(type);
if (displayName != null)
return displayName;
return type.Name;
}
return entityName;
}
private readonly Dictionary<string, Type> cacheDictionary = new();
private Type? GetTypeFromFullName(string className)
{
if (cacheDictionary.TryGetValue(className, out var name))
return name;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.FullName == null ||
!type.FullName.Equals(className, StringComparison.OrdinalIgnoreCase))
continue;
cacheDictionary.Add(className, type);
return type;
}
}
return null;
}
private static string? GetDisplayName(Type type)
{
return (from attribute in type.CustomAttributes where attribute.AttributeType == typeof(DisplayNameAttribute) select attribute.ConstructorArguments[0].ToString().Trim('"')).FirstOrDefault();
}
public long Id { get; set; }
public long UserId { get; set; }

View File

@ -2,7 +2,19 @@
public class BlockedIPs
{
public string IPAddress { get; set; } = string.Empty;
public BlockedIPs()
{
}
public BlockedIPs(BlockedIPs blockedIPs)
{
IpAddress = blockedIPs.IpAddress;
NumberOfAttempts = blockedIPs.NumberOfAttempts;
BlockedAt = blockedIPs.BlockedAt;
UnblockedIn = blockedIPs.UnblockedIn;
}
public string IpAddress { get; set; } = string.Empty;
public int NumberOfAttempts { get; set; }

View File

@ -2,6 +2,21 @@
public class ExceptionLog
{
public ExceptionLog()
{
}
public ExceptionLog(e_suite.Database.Core.Tables.Diagnostics.ExceptionLog exceptionLog)
{
Id = exceptionLog.Id;
Application = exceptionLog.Application;
ExceptionJson = exceptionLog.ExceptionJson;
Message = exceptionLog.Message;
OccuredAt = exceptionLog.OccuredAt;
StackTrace = exceptionLog.StackTrace;
SupportingData = exceptionLog.SupportingData;
}
public long Id { get; set; }
public string Application { get; set; } = string.Empty;

View File

@ -1,10 +1,27 @@
using eSuite.Core.CustomFields;
using e_suite.Database.Core.Tables.CustomFields;
using eSuite.Core.CustomFields;
using eSuite.Core.Miscellaneous;
namespace e_suite.API.Common.models;
public class CustomFieldDefinition
{
public CustomFieldDefinition()
{
}
public CustomFieldDefinition(CustomField customField)
{
Id = customField.Id;
Guid = customField.Guid;
Name = customField.Name;
FieldType = customField.FieldType;
DefaultValue = customField.DefaultValue;
MinEntries = customField.MinEntries;
MaxEntries = customField.MaxEntries;
}
public long Id { get; set; }
public string Name { get; set; } = string.Empty;

View File

@ -1,11 +1,30 @@
using System.Text.Json.Serialization;
using e_suite.API.Common.models.@base;
using e_suite.Database.Core.Extensions;
using e_suite.Database.Core.Models;
using e_suite.Database.Core.Tables.Domain;
namespace e_suite.API.Common.models;
public class GetDomain : DomainBase, IGeneralId
{
public GetDomain() : base()
{
}
public GetDomain(Domain domain)
{
Guid = domain.Guid;
Id = domain.Id;
Name = domain.Name;
SsoProviderId = domain.SsoProvider?.ToGeneralIdRef();
SunriseHostName = domain.SunriseHostname;
SunriseAppId = domain.SunriseAppId;
SunriseCategoryId = domain.SunriseCategoryId;
SigmaId = domain.SigmaId;
}
[JsonPropertyName("id")]
public long Id { get; set; }

View File

@ -1,9 +1,24 @@
using e_suite.Database.Core.Models;
using e_suite.Database.Core.Tables.Printer;
namespace e_suite.API.Common.models;
public class ReadOrganisation : IGeneralId
{
public ReadOrganisation()
{
}
public ReadOrganisation(Organisation organisation)
{
Guid = organisation.Guid;
Id = organisation.Id;
Name = organisation.Name;
Address = organisation.Address;
Status = organisation.Status;
}
public long Id { get; set; }
public Guid Guid { get; set; }

View File

@ -0,0 +1,8 @@
using e_suite.Database.Core;
namespace e_suite.API.Common.repository;
public interface IWorkflowTemplateRepository : IRepository
{
}

View File

@ -1,5 +1,4 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using e_suite.Database.Audit.Attributes;
@ -32,5 +31,5 @@ public class FormTemplate : IGeneralId, ISoftDeletable
[AuditLastUpdated]
public DateTimeOffset LastUpdated { get; set; }
public ICollection<FormTemplateVersion> Versions { get; set; } = (Collection<FormTemplateVersion>) [];
public ICollection<FormTemplateVersion> Versions { get; set; } = [];
}

View File

@ -7,7 +7,6 @@ using e_suite.Utilities.Pagination;
using eSuite.Core.Clock;
using Microsoft.Extensions.Configuration;
using System.Linq.Expressions;
using MockQueryable;
namespace e_suite.Modules.BlockedIPsManager;
@ -37,25 +36,19 @@ public class BlockedIPsManager : IBlockedIPsManager
var dateTimeNow = _clock.GetNow;
var data = blockedIPs.GroupBy(g => g.IPAddress)
.Select(x => new BlockedIPs {
IPAddress = x.First().IPAddress,
.Select(x => new BlockedIPs
{
IpAddress = x.First().IPAddress,
NumberOfAttempts = x.Count(),
BlockedAt = x.Max(t => t.AttemptedTime),
UnblockedIn = GetUnlockedMinutes(x.Min(t => t.AttemptedTime), loginAttemptTimeoutMinutes, dateTimeNow).ToInteger()
})
.ToList()
.BuildMock();
UnblockedIn = GetUnlockedMinutes(x.Min(t => t.AttemptedTime), loginAttemptTimeoutMinutes, dateTimeNow)
.ToInteger()
});
var paginatedData = await PaginatedData.Paginate(data, paging,
var paginatedData = await PaginatedData.Paginate<BlockedIPs, BlockedIPs>(data, paging,
KeySelector, FilterSelector, cancellationToken);
return new PaginatedData<BlockedIPs>
{
Count = paginatedData.Count,
Page = paginatedData.Page,
PageSize = paginatedData.PageSize,
Data = paginatedData.Data
};
return paginatedData;
}
public static double GetUnlockedMinutes(DateTimeOffset attemptedTime, int loginAttemptTimeoutMinutes, DateTimeOffset dateTimeNow)
@ -67,11 +60,11 @@ public class BlockedIPsManager : IBlockedIPsManager
{
return key?.ToLowerInvariant() switch
{
"ipaddress" => x => x.IPAddress.ToString().Contains(value),
"ipaddress" => x => x.IpAddress.ToString().Contains(value),
"numberOfAttempts" => x => x.NumberOfAttempts.ToString().Contains(value),
"blockedAt" => x => x.BlockedAt.ToString().Contains(value),
"unblockedin" => x => x.UnblockedIn.ToString().Contains(value),
_ => x => x.IPAddress.ToString().Contains(value)
_ => x => x.IpAddress.ToString().Contains(value)
};
}
@ -79,11 +72,11 @@ public class BlockedIPsManager : IBlockedIPsManager
{
return sortKey?.ToLowerInvariant() switch
{
"ipaddress" => x => x.IPAddress,
"ipaddress" => x => x.IpAddress,
"numberofattempts" => x => x.NumberOfAttempts,
"blockedat" => x => x.BlockedAt,
"unblockedin" => x => x.UnblockedIn,
_ => x => x.IPAddress
_ => x => x.IpAddress
};
}

View File

@ -7,10 +7,6 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MockQueryable.Moq" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\e-suite.API.Common\e-suite.API.Common\e-suite.API.Common.csproj" />
<ProjectReference Include="..\..\e-suite.DependencyInjection\e-suite.DependencyInjection.csproj" />

View File

@ -30,74 +30,8 @@ public class AuditLog : IAuditLog
var auditEntries = _auditLogRepository.GetAuditEntries(auditParams, primaryOnly);
var paginatedData =
await PaginatedData.Paginate(auditEntries, paging, KeySelector, FilterSelector, cancellationToken);
var paginatedResult = new PaginatedData<AuditLogEntry>
{
Count = paginatedData.Count,
Page = paginatedData.Page,
PageSize = paginatedData.PageSize,
Data = paginatedData.Data.Select(x => new AuditLogEntry
{
Id = x.Id,
UserId = x.AuditLog.UserId,
UserDisplayName = x.AuditLog.UserDisplayName,
Type = x.AuditLog.Type,
DateTime = x.AuditLog.DateTime,
Fields = x.AuditLog.Fields,
Comment = x.AuditLog.Comment,
EntityName = x.EntityName,
EntityDisplayName = GetDisplayNameForClass( x.EntityName ),
PrimaryKey = x.PrimaryKey,
DisplayName = x.DisplayName ?? string.Empty
})
};
return paginatedResult;
}
private string GetDisplayNameForClass(string entityName)
{
var type = GetTypeFromFullName(entityName);
if (type != null)
{
var displayName = GetDisplayName(type);
if (displayName != null)
return displayName;
return type.Name;
}
return entityName;
}
private static string? GetDisplayName(Type type)
{
return (from attribute in type.CustomAttributes where attribute.AttributeType == typeof(DisplayNameAttribute) select attribute.ConstructorArguments[0].ToString().Trim('"')).FirstOrDefault();
}
private readonly Dictionary<string, Type> cacheDictionary = new();
private Type? GetTypeFromFullName(string className)
{
if (cacheDictionary.TryGetValue(className, out var name))
return name;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.FullName == null ||
!type.FullName.Equals(className, StringComparison.OrdinalIgnoreCase))
continue;
cacheDictionary.Add(className, type);
return type;
}
}
return null;
await PaginatedData.Paginate<AuditEntry, AuditLogEntry>(auditEntries, paging, KeySelector, FilterSelector, cancellationToken);
return paginatedData;
}
private Expression<Func<AuditEntry, bool>> FilterSelector(string? key, string value)

View File

@ -124,26 +124,10 @@ public class CustomFieldManager : ICustomFieldManager
{
var sequences = _customFieldRepository.GetCustomFieldList();
var paginatedData = await PaginatedData.Paginate(sequences, paging,
var paginatedData = await PaginatedData.Paginate<CustomField, CustomFieldDefinition>(sequences, paging,
KeySelector, FilterSelector, cancellationToken);
var paginatedResult = new PaginatedData<CustomFieldDefinition>
{
Count = paginatedData.Count,
Page = paginatedData.Page,
PageSize = paginatedData.PageSize,
Data = paginatedData.Data.Select(x => new CustomFieldDefinition
{
Id = x.Id,
Guid = x.Guid,
Name = x.Name,
DefaultValue = x.DefaultValue,
FieldType = x.FieldType,
})
};
return paginatedResult;
return paginatedData;
}
private Expression<Func<CustomField, bool>> FilterSelector(string? key, string value)

View File

@ -26,17 +26,10 @@ namespace e_suite.Modules.DomainManager
{
var forms = _domainRepository.GetDomains();
var paginatedData = await PaginatedData.Paginate(forms, paging,
var paginatedData = await PaginatedData.Paginate<Domain, GetDomain>(forms, paging,
KeySelector, FilterSelector, cancellationToken);
var paginatedResult = new PaginatedData<GetDomain>
{
Count = paginatedData.Count,
Page = paginatedData.Page,
PageSize = paginatedData.PageSize,
Data = paginatedData.Data.Select(MapDomain)
};
return paginatedResult;
return paginatedData;
}
private Expression<Func<Domain, bool>> FilterSelector(string? key, string value)
@ -61,26 +54,11 @@ namespace e_suite.Modules.DomainManager
};
}
private static GetDomain MapDomain(Domain domain)
{
return new GetDomain
{
Guid = domain.Guid,
Id = domain.Id,
Name = domain.Name,
SsoProviderId = domain.SsoProvider?.ToGeneralIdRef(),
SunriseHostName = domain.SunriseHostname,
SunriseAppId = domain.SunriseAppId,
SunriseCategoryId = domain.SunriseCategoryId,
SigmaId = domain.SigmaId,
};
}
public async Task<GetDomain> GetDomainAsync(IGeneralIdRef generalIdRef, CancellationToken cancellationToken)
{
var domain = await _domainRepository.GetDomainById(generalIdRef, cancellationToken);
AssertDomain(domain);
return MapDomain(domain!);
return new GetDomain(domain!);
}
public async Task CreateDomainAsync(AuditUserDetails auditUserDetails, CreateDomain createDomain,

View File

@ -1,38 +0,0 @@
using e_suite.Modules.ExceptionLogManager.Extensions;
using e_suite.Modules.ExceptionLogManager.UnitTests.Helpers;
using Newtonsoft.Json;
using NUnit.Framework;
namespace e_suite.Modules.ExceptionLogManager.UnitTests;
public class ExtensionUnitTests: ExceptionLogManagerTestBase
{
[SetUp]
public override async Task Setup()
{
await base.Setup();
}
[Test]
public void DoubleExtensionMethod_WhenCalled_ReturnsExpectedResult()
{
//Arrange
var ex = new NullReferenceException();
var exception = new Database.Core.Tables.Diagnostics.ExceptionLog
{
Application = "E-Suite API",
Message = "Internal Server Error",
StackTrace = ex.StackTrace ?? string.Empty,
ExceptionJson = JsonConvert.SerializeObject(ex),
SupportingData = string.Empty,
OccuredAt = _fakeClock.GetNow
};
//Act
var result = exception.ToExceptionLog();
//Assert
Assert.That(result.GetType().Name, Is.EqualTo("ExceptionLog"));
Assert.Pass();
}
}

View File

@ -1,7 +1,6 @@
using e_suite.API.Common;
using e_suite.API.Common.models;
using e_suite.API.Common.repository;
using e_suite.Modules.ExceptionLogManager.Extensions;
using e_suite.Utilities.Pagination;
using eSuite.Core.Clock;
using Newtonsoft.Json;
@ -25,16 +24,10 @@ public class ExceptionLogger : IExceptionLogManager
{
var exceptionLogs = _exceptionLogRepository.GetExceptionLogs();
var paginatedData = await PaginatedData.Paginate(exceptionLogs, paging,
var paginatedData = await PaginatedData.Paginate<e_suite.Database.Core.Tables.Diagnostics.ExceptionLog, ExceptionLog>(exceptionLogs, paging,
KeySelector, FilterSelector, cancellationToken);
return new PaginatedData<ExceptionLog>
{
Count = paginatedData.Count,
Page = paginatedData.Page,
PageSize = paginatedData.PageSize,
Data = paginatedData.Data.Select(x => x.ToExceptionLog())
};
return paginatedData;
}
[ExcludeFromCodeCoverage]

View File

@ -1,20 +0,0 @@
using e_suite.API.Common.models;
namespace e_suite.Modules.ExceptionLogManager.Extensions;
public static class ExceptionLogExtension
{
public static ExceptionLog ToExceptionLog(this Database.Core.Tables.Diagnostics.ExceptionLog exceptionLog)
{
return new ExceptionLog
{
Id = exceptionLog.Id,
Application = exceptionLog.Application,
ExceptionJson = exceptionLog.ExceptionJson,
Message = exceptionLog.Message,
OccuredAt = exceptionLog.OccuredAt,
StackTrace = exceptionLog.StackTrace,
SupportingData = exceptionLog.SupportingData
};
}
}

View File

@ -137,7 +137,6 @@ public class FormsManager : IFormsManager
var paginatedData = await PaginatedData.Paginate(forms, paging,
KeySelector, FilterSelector, cancellationToken);
var mappedData = new List<GetFormTemplate>();
foreach (var item in paginatedData.Data)
@ -159,7 +158,11 @@ public class FormsManager : IFormsManager
{
"id" => x => x.Id.ToString().Contains(value),
"guid" => x => x.Guid.ToString().Contains(value),
"version" => x => x.Versions.OrderByDescending(x => x.Version).First( v => v.Deleted == false).Version.ToString().Contains(value),
"version" => x => x.Versions
.Where(v => !v.Deleted)
.Max(v => v.Version)
.ToString()
.Contains(value),
_ => x => x.Name.Contains(value)
};
}
@ -170,7 +173,9 @@ public class FormsManager : IFormsManager
{
"id" => x => x.Id,
"guid" => x => x.Guid,
"version" => x => x.Versions.OrderByDescending(x=>x.Version).First(x => !x.Deleted).Version,
"version" => x => x.Versions
.Where(v => !v.Deleted)
.Max(v => v.Version),
_ => x => x.Name
};
}

View File

@ -57,24 +57,10 @@ public class OrganisationsManager : IOrganisationsManager
{
var organisations = _organisationsManagerRepository.GetOrganisationsList();
var paginatedData = await PaginatedData.Paginate(organisations, paging,
var paginatedData = await PaginatedData.Paginate<Organisation, ReadOrganisation>(organisations, paging,
KeySelector, FilterSelector, cancellationToken);
var paginatedResult = new PaginatedData<ReadOrganisation>
{
Count = paginatedData.Count,
Page = paginatedData.Page,
PageSize = paginatedData.PageSize,
Data = paginatedData.Data.Select(x => new ReadOrganisation
{
Guid = x.Guid,
Id = x.Id,
Name = x.Name,
Address = x.Address,
Status = x.Status,
})
};
return paginatedResult;
return paginatedData;
}
private Expression<Func<Organisation, bool>> FilterSelector(string? key, string value)

View File

@ -25,7 +25,7 @@ using e_suite.API.Common;
namespace e_suite.Modules.UserManager;
public partial class UserManager : IUserManager
public class UserManager : IUserManager
{
private readonly IConfiguration _configuration;
private readonly IPasswordHasher<IPassword> _passwordHasher;

View File

@ -1,7 +0,0 @@
namespace e_suite.Modules.WorkflowTemplatesManager
{
public class Class1
{
}
}

View File

@ -0,0 +1,11 @@
using e_suite.API.Common.repository;
using e_suite.Database.Core;
namespace e_suite.Modules.WorkflowTemplatesManager.Repository;
public class WorkflowTemplateRepository : RepositoryBase, IWorkflowTemplateRepository
{
public WorkflowTemplateRepository(IEsuiteDatabaseDbContext databaseDbContext) : base(databaseDbContext)
{
}
}

View File

@ -0,0 +1,8 @@
using e_suite.API.Common;
namespace e_suite.Modules.WorkflowTemplatesManager;
public class WorkflowTemplateManager : IWorkflowTemplateManager
{
}

View File

@ -15,8 +15,13 @@ public class PaginatedData<T> : IPaginatedData<T>
public static class PaginatedData
{
public static async Task<PaginatedData<T>> Paginate<T>(IQueryable<T> queryable, Paging paging, Func<string, Expression<Func<T, object>>> keySelector,
Func<string, string, Expression<Func<T, bool>>> filterSelector, CancellationToken cancellationToken)
public static async Task<PaginatedData<T>> Paginate<T>(
IQueryable<T> queryable,
Paging paging,
Func<string, Expression<Func<T, object>>> keySelector,
Func<string, string, Expression<Func<T, bool>>> filterSelector,
CancellationToken cancellationToken
)
{
var filteredData = ApplyFilters(queryable, paging.Filters, filterSelector);
@ -31,10 +36,62 @@ public static class PaginatedData
if (paginated.Page < 1) paginated.Page = 1;
var sortedData = ApplySort(filteredData, paging.SortKey, paging.SortAscending, keySelector);
paginated.Data = paging.Page == 0 ? sortedData : sortedData.Skip((paginated.Page - 1) * paginated.PageSize).Take(paginated.PageSize);
paginated.Data = paging.Page == 0
? sortedData
: sortedData.Skip((paginated.Page - 1) * paginated.PageSize).Take(paginated.PageSize);
return paginated;
}
public static async Task<PaginatedData<TOutput>> Paginate<TInput, TOutput>(
IQueryable<TInput> queryable,
Paging paging,
Func<string, Expression<Func<TInput, object>>> keySelector,
Func<string, string, Expression<Func<TInput, bool>>> filterSelector,
CancellationToken cancellationToken
)
{
var paged = await Paginate(queryable, paging, keySelector, filterSelector, cancellationToken);
var result = new PaginatedData<TOutput>
{
Count = paged.Count,
Page = paged.Page,
PageSize = paged.PageSize,
Data = paged.Data.Select(item => (TOutput)Activator.CreateInstance(typeof(TOutput), item)!)
};
return result;
}
public static async Task<PaginatedData<TOutput>> Paginate<TInput, TOutput>(
IQueryable<TInput> queryable,
Paging paging,
Func<string, Expression<Func<TInput, object>>> keySelector,
Func<string, string, Expression<Func<TInput, bool>>> filterSelector,
CancellationToken cancellationToken,
params object[] args
)
{
var paged = await Paginate(queryable, paging, keySelector, filterSelector, cancellationToken);
var result = new PaginatedData<TOutput>
{
Count = paged.Count,
Page = paged.Page,
PageSize = paged.PageSize,
Data = paged.Data.Select(item =>
{
var allArgs = new List<object> { item! };
allArgs.AddRange(args);
return (TOutput)Activator.CreateInstance(typeof(TOutput), allArgs.ToArray())!;
})
};
return result;
}
private static IQueryable<T> ApplyFilters<T>(IQueryable<T> queryable, string filters,
Func<string, string, Expression<Func<T, bool>>> filterSelector)
{

View File

@ -1,21 +1,30 @@
using e_suite.Workflow.Core.Enums;
using e_suite.Database.Core.Models;
using e_suite.Workflow.Core.Enums;
using e_suite.Workflow.Core.Interfaces;
using eSuite.Core.Miscellaneous;
namespace e_suite.Workflow.Core;
public class WorkflowTemplate
public class WorkflowTemplate: IGeneralId
{
public long Id { get; set; }
public Guid Guid { get; set; }
/// <summary>
/// Name of the workflow as seen by users, must be unique.
/// </summary>
public required string Name { get; set; }
}
public class WorkflowVersion : IWorkflow
public class WorkflowVersion : IGeneralId, IWorkflow
{
public long Id { get; set; }
public Guid Guid { get; set; }
public required WorkflowTemplate Template { get; set; }
public required long Version { get; set; }
public ICollection<ITask> Tasks { get; } = new List<ITask>(); //Serialise to Json.
public required IGeneralIdRef Domain { get; set; }
public WorkflowState CurrentState { get; set; } = WorkflowState.Pending;