diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7016c13..d000348 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: branches: [ "main" ] env: - PACKAGE_VERSION: 1.2.13 + PACKAGE_VERSION: 1.3.0 jobs: diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj index 378aa65..2d59764 100644 --- a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj +++ b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj @@ -21,7 +21,7 @@ - + diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj index 697d9f5..58c7d33 100644 --- a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj +++ b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs b/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs new file mode 100644 index 0000000..9ab9a70 --- /dev/null +++ b/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs @@ -0,0 +1,30 @@ +using CloudFabric.EventSourcing.EventStore; + +namespace CloudFabric.EAV.Domain.Models; + +public record CategoryCreated : Event +{ + public CategoryCreated() + { + + } + + public CategoryCreated(Guid id, + string machineName, + Guid entityConfigurationId, + List attributes, + Guid? tenantId) + { + TenantId = tenantId; + Attributes = attributes; + EntityConfigurationId = entityConfigurationId; + AggregateId = id; + MachineName = machineName; + } + + public Guid EntityConfigurationId { get; set; } + public List Attributes { get; set; } + public Guid? TenantId { get; set; } + public string MachineName { get; set; } + +} diff --git a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs index 990ca06..11cfe3d 100644 --- a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs +++ b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs @@ -10,16 +10,24 @@ public EntityCategoryPathChanged() { } - public EntityCategoryPathChanged(Guid id, Guid entityConfigurationId, Guid categoryTreeId, string categoryPath) + public EntityCategoryPathChanged(Guid id, + Guid entityConfigurationId, + Guid categoryTreeId, + string categoryPath, + Guid? parentId) { AggregateId = id; EntityConfigurationId = entityConfigurationId; CategoryPath = categoryPath; CategoryTreeId = categoryTreeId; + ParentId = parentId; + ParentMachineName = string.IsNullOrEmpty(categoryPath) ? "" : categoryPath.Split('/').Last(x => !string.IsNullOrEmpty(x)); } public string CategoryPath { get; set; } public Guid EntityConfigurationId { get; set; } public Guid CategoryTreeId { get; set; } + public Guid? ParentId { get; set; } + public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs b/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs index 1c1afc8..ba5eb31 100644 --- a/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs +++ b/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs @@ -1,5 +1,7 @@ using System.Text.RegularExpressions; +using CloudFabric.EAV.Domain.Utilities.Extensions; + namespace CloudFabric.EAV.Domain.Models.Attributes; public class ValueFromListOptionConfiguration @@ -11,9 +13,7 @@ public ValueFromListOptionConfiguration(string name, string? machineName) if (string.IsNullOrEmpty(machineName)) { - machineName = name.Replace(" ", "_"); - var specSymbolsRegex = new Regex("[^\\d\\w_]*", RegexOptions.None, TimeSpan.FromMilliseconds(100)); - machineName = specSymbolsRegex.Replace(machineName, "").ToLower(); + machineName = name.SanitizeForMachineName(); } MachineName = machineName; diff --git a/CloudFabric.EAV.Domain/Models/Category.cs b/CloudFabric.EAV.Domain/Models/Category.cs index 8d9f539..b7fb109 100644 --- a/CloudFabric.EAV.Domain/Models/Category.cs +++ b/CloudFabric.EAV.Domain/Models/Category.cs @@ -5,24 +5,42 @@ namespace CloudFabric.EAV.Domain.Models; public class Category : EntityInstanceBase { + public string MachineName { get; set; } public Category(IEnumerable events) : base(events) { } - public Category(Guid id, Guid entityConfigurationId, List attributes, Guid? tenantId) - : base(id, entityConfigurationId, attributes, tenantId) + public Category(Guid id, + string machineName, + Guid entityConfigurationId, + List attributes, + Guid? tenantId) { + Apply(new CategoryCreated(id, machineName, entityConfigurationId, attributes, tenantId)); + } public Category( Guid id, + string machineName, Guid entityConfigurationId, List attributes, Guid? tenantId, string categoryPath, + Guid? parentId, Guid categoryTreeId - ) : base(id, entityConfigurationId, attributes, tenantId) + ) : this(id, machineName, entityConfigurationId, attributes, tenantId) + { + Apply(new EntityCategoryPathChanged(id, EntityConfigurationId, categoryTreeId, categoryPath, parentId)); + } + + public void On(CategoryCreated @event) { - Apply(new EntityCategoryPathChanged(id, EntityConfigurationId, categoryTreeId, categoryPath)); + Id = @event.AggregateId; + EntityConfigurationId = @event.EntityConfigurationId; + Attributes = new List(@event.Attributes).AsReadOnly(); + TenantId = @event.TenantId; + CategoryPaths = new List(); + MachineName = @event.MachineName; } } diff --git a/CloudFabric.EAV.Domain/Models/CategoryPath.cs b/CloudFabric.EAV.Domain/Models/CategoryPath.cs index 608929e..1b2ac03 100644 --- a/CloudFabric.EAV.Domain/Models/CategoryPath.cs +++ b/CloudFabric.EAV.Domain/Models/CategoryPath.cs @@ -4,4 +4,6 @@ public class CategoryPath { public Guid TreeId { get; set; } public string Path { get; set; } + public Guid? ParentId { get; set; } + public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs index 99f78a7..3ffa85a 100644 --- a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs +++ b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs @@ -9,7 +9,7 @@ namespace CloudFabric.EAV.Domain.Models; public class EntityInstanceBase : AggregateBase { - public EntityInstanceBase(IEnumerable events) : base(events) + public EntityInstanceBase(IEnumerable events) : base(events) { } @@ -19,6 +19,10 @@ public EntityInstanceBase(Guid id, Guid entityConfigurationId, List EntityConfigurationId.ToString(); public List CategoryPaths { get; protected set; } @@ -54,9 +58,9 @@ public void On(EntityInstanceCreated @event) CategoryPaths = new List(); } - public void ChangeCategoryPath(Guid treeId, string categoryPath) + public void ChangeCategoryPath(Guid treeId, string categoryPath, Guid parentId) { - Apply(new EntityCategoryPathChanged(Id, EntityConfigurationId, treeId, categoryPath)); + Apply(new EntityCategoryPathChanged(Id, EntityConfigurationId, treeId, categoryPath, parentId)); } public void On(EntityCategoryPathChanged @event) @@ -64,11 +68,17 @@ public void On(EntityCategoryPathChanged @event) CategoryPath? categoryPath = CategoryPaths.FirstOrDefault(x => x.TreeId == @event.CategoryTreeId); if (categoryPath == null) { - CategoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId, Path = @event.CategoryPath }); + CategoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId, + Path = @event.CategoryPath, + ParentId = @event.ParentId, + ParentMachineName = @event.ParentMachineName + }); } else { categoryPath.Path = @event.CategoryPath; + categoryPath.ParentMachineName = @event.ParentMachineName; + categoryPath.ParentId = @event.ParentId; } } diff --git a/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs index b2827e4..8bc5d8e 100644 --- a/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs @@ -26,8 +26,9 @@ public class AttributeConfigurationProjectionBuilder : ProjectionBuilder { public AttributeConfigurationProjectionBuilder( - ProjectionRepositoryFactory projectionRepositoryFactory, AggregateRepositoryFactory _ - ) : base(projectionRepositoryFactory) + ProjectionRepositoryFactory projectionRepositoryFactory, + ProjectionOperationIndexSelector indexSelector + ) : base(projectionRepositoryFactory, indexSelector) { } diff --git a/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs index 8a96c1d..1dded58 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs @@ -15,8 +15,9 @@ public class EntityConfigurationProjectionBuilder : ProjectionBuilder> { public EntityConfigurationProjectionBuilder( - ProjectionRepositoryFactory projectionRepositoryFactory, AggregateRepositoryFactory _ - ) : base(projectionRepositoryFactory) + ProjectionRepositoryFactory projectionRepositoryFactory, + ProjectionOperationIndexSelector indexSelector + ) : base(projectionRepositoryFactory, indexSelector) { } diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs index 27da37e..fe2393f 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs @@ -31,7 +31,8 @@ namespace CloudFabric.EAV.Domain.Projections.EntityInstanceProjection; /// public class EntityInstanceProjectionBuilder : ProjectionBuilder, IHandleEvent, - // IHandleEvent, + IHandleEvent, +// IHandleEvent, IHandleEvent, // IHandleEvent, IHandleEvent, @@ -41,8 +42,9 @@ public class EntityInstanceProjectionBuilder : ProjectionBuilder, public EntityInstanceProjectionBuilder( ProjectionRepositoryFactory projectionRepositoryFactory, - AggregateRepositoryFactory aggregateRepositoryFactory - ) : base(projectionRepositoryFactory) + AggregateRepositoryFactory aggregateRepositoryFactory, + ProjectionOperationIndexSelector indexSelector + ) : base(projectionRepositoryFactory, indexSelector) { _aggregateRepositoryFactory = aggregateRepositoryFactory; } @@ -134,14 +136,20 @@ await UpdateDocument( List categoryPaths = categoryPathsObj as List ?? new List(); CategoryPath? categoryPath = categoryPaths.FirstOrDefault(x => x.TreeId == @event.CategoryTreeId); + if (categoryPath == null) { - categoryPaths.Add(new CategoryPath { Path = @event.CategoryPath, TreeId = @event.CategoryTreeId } - ); + categoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId, + Path = @event.CategoryPath, + ParentId = @event.ParentId, + ParentMachineName = @event.ParentMachineName + }); } else { categoryPath.Path = @event.CategoryPath; + categoryPath.ParentMachineName = @event.ParentMachineName; + categoryPath.ParentId = @event.ParentId; } document["CategoryPaths"] = categoryPaths; @@ -177,6 +185,35 @@ await UpsertDocument( ); } + public async Task On(CategoryCreated @event) + { + ProjectionDocumentSchema projectionDocumentSchema = + await BuildProjectionDocumentSchemaForEntityConfigurationIdAsync( + @event.EntityConfigurationId + ).ConfigureAwait(false); + + var document = new Dictionary + { + { "Id", @event.AggregateId }, + { "EntityConfigurationId", @event.EntityConfigurationId }, + { "TenantId", @event.TenantId }, + { "CategoryPaths", new List() }, + { "MachineName", @event.MachineName}, + }; + + foreach (AttributeInstance attribute in @event.Attributes) + { + document.Add(attribute.ConfigurationAttributeMachineName, attribute.GetValue()); + } + + await UpsertDocument( + projectionDocumentSchema, + document, + @event.PartitionKey, + @event.Timestamp + ); + } + private async Task BuildProjectionDocumentSchemaForEntityConfigurationIdAsync( Guid entityConfigurationId ) diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs index 3ce1e85..3c7338f 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs @@ -240,6 +240,7 @@ public static ProjectionDocumentPropertySchema GetArrayAttributeSchema( EavAttributeType.HtmlText => null, EavAttributeType.EntityReference => null, EavAttributeType.ValueFromList => null, + EavAttributeType.Money => null, EavAttributeType.LocalizedText => GetLocalizedTextAttributeNestedProperties(), EavAttributeType.DateRange => GetDateAttributeNestedProperties(), EavAttributeType.Image => GetImageAttributeNestedProperties(), @@ -441,6 +442,22 @@ private static List GetCategoryPathsNestedProp IsRetrievable = true, IsFilterable = true, IsSortable = true + }, + new () + { + PropertyName = "ParentMachineName", + PropertyType = TypeCode.String, + IsRetrievable = true, + IsFilterable = true, + IsSortable = true + }, + new () + { + PropertyName = "ParentId", + PropertyType = TypeCode.Object, + IsRetrievable = true, + IsFilterable = true, + IsSortable = true } }; } diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs index f5e978e..e94771d 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs @@ -44,10 +44,25 @@ List attributeConfigurations } ); + schema.Properties.Add( + new ProjectionDocumentPropertySchema + { + PropertyName = "MachineName", + PropertyType = TypeCode.String, + IsKey = false, + IsSearchable = true, + IsRetrievable = true, + IsFilterable = true, + IsSortable = false, + IsFacetable = false + } + ); + schema.Properties.Add( ProjectionAttributesSchemaFactory.GetCategoryPathsAttributeSchema() ); + schema.Properties.Add( new ProjectionDocumentPropertySchema { diff --git a/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs b/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs new file mode 100644 index 0000000..6adf242 --- /dev/null +++ b/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs @@ -0,0 +1,12 @@ +using System.Text.RegularExpressions; + +namespace CloudFabric.EAV.Domain.Utilities.Extensions; + +public static class StringExtensions +{ + public static string SanitizeForMachineName(this string str) + { + var specSymbolsRegex = new Regex("[^\\d\\w_]*", RegexOptions.None, TimeSpan.FromMilliseconds(100)); + return specSymbolsRegex.Replace(str.Replace(" ", "_"), "").ToLower(); + } +} diff --git a/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj b/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj index cf7fb5d..4cd4100 100644 --- a/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj +++ b/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj @@ -20,7 +20,7 @@ - + diff --git a/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs b/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs index 4db28c8..86f1f55 100644 --- a/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs +++ b/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs @@ -6,6 +6,7 @@ public class CategoryInstanceCreateRequest public Guid CategoryTreeId { get; set; } + public string MachineName { get; set; } public List Attributes { get; set; } public Guid? ParentId { get; set; } diff --git a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs index 00d6477..1eec2ea 100755 --- a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs +++ b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs @@ -9,3 +9,8 @@ public class EntityInstanceUpdateRequest public List AttributesToAddOrUpdate { get; set; } public List? AttributeMachineNamesToRemove { get; set; } } + +public class CategoryUpdateRequest: EntityInstanceUpdateRequest +{ + +} diff --git a/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs index 68375bf..22708b3 100644 --- a/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs @@ -4,4 +4,6 @@ public class CategoryPathViewModel { public Guid TreeId { get; set; } public string Path { get; set; } + public Guid? ParentId { get; set; } + public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs index a1aa458..b224d60 100644 --- a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs @@ -3,8 +3,8 @@ namespace CloudFabric.EAV.Models.ViewModels; // As the domain model EntityInstanceBase presents a vast array of features // and represents both EntityInstance and Category with the same properties set, // it is preferable for one of the models to be inherited from another, -// in order to avoid code overload and repeats. +// in order to avoid code overload and repeats. public class CategoryViewModel : EntityInstanceViewModel { - + public string MachineName { get; set; } } diff --git a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs index 4f8e156..da8f9f4 100755 --- a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs @@ -21,6 +21,8 @@ public class EntityTreeInstanceViewModel { public Guid Id { get; set; } + public string MachineName { get; set; } + public Guid EntityConfigurationId { get; set; } public List Attributes { get; set; } diff --git a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj index ca82788..f7a353d 100644 --- a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj +++ b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj @@ -13,7 +13,7 @@ - + diff --git a/CloudFabric.EAV.Service/EAVCategoryService.cs b/CloudFabric.EAV.Service/EAVCategoryService.cs new file mode 100644 index 0000000..e9cdafa --- /dev/null +++ b/CloudFabric.EAV.Service/EAVCategoryService.cs @@ -0,0 +1,783 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +using AutoMapper; + +using CloudFabric.EAV.Domain.Models; +using CloudFabric.EAV.Models.RequestModels; +using CloudFabric.EAV.Models.ViewModels; +using CloudFabric.EAV.Options; +using CloudFabric.EAV.Service.Serialization; +using CloudFabric.EventSourcing.Domain; +using CloudFabric.EventSourcing.EventStore; +using CloudFabric.EventSourcing.EventStore.Persistence; +using CloudFabric.Projections; +using CloudFabric.Projections.Queries; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using ProjectionDocumentSchemaFactory = + CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; + +namespace CloudFabric.EAV.Service; + +public class EAVCategoryService: EAVService +{ + + private readonly ElasticSearchQueryOptions _elasticSearchQueryOptions; + + public EAVCategoryService(ILogger> logger, + IMapper mapper, + JsonSerializerOptions jsonSerializerOptions, + AggregateRepositoryFactory aggregateRepositoryFactory, + ProjectionRepositoryFactory projectionRepositoryFactory, + EventUserInfo userInfo, + IOptions? elasticSearchQueryOptions = null) : base(logger, + new CategoryFromDictionaryDeserializer(mapper), + mapper, + jsonSerializerOptions, + aggregateRepositoryFactory, + projectionRepositoryFactory, + userInfo) + { + + _elasticSearchQueryOptions = elasticSearchQueryOptions != null + ? elasticSearchQueryOptions.Value + : new ElasticSearchQueryOptions(); + } + + + #region Categories + + public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync( + CategoryTreeCreateRequest entity, + Guid? tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + entity.EntityConfigurationId, + entity.EntityConfigurationId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; + } + + var tree = new CategoryTree( + Guid.NewGuid(), + entity.EntityConfigurationId, + entity.MachineName, + tenantId + ); + + _ = await _categoryTreeRepository.SaveAsync(_userInfo, tree, cancellationToken).ConfigureAwait(false); + return (_mapper.Map(tree), null)!; + } + + /// + /// Create new category from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", + /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, + /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories + /// "parentId" - id guid of category from which new branch of hierarchy will be built. + /// Can be null if placed at the root of category tree. + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + string categoryJsonString, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); + + return CreateCategoryInstance( + categoryJson.RootElement, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names. + /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// + /// id of entity configuration which has all category attributes + /// id of category tree, which represents separated hirerarchy with relations between categories + /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. + /// tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + string categoryJsonString, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); + + return CreateCategoryInstance( + categoryJson.RootElement, + machineName, + categoryConfigurationId, + categoryTreeId, + parentId, + tenantId, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", + /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, + /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories + /// "parentId" - id guid of category from which new branch of hierarchy will be built. + /// Can be null if placed at the root of category tree. + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + JsonElement categoryJson, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + var (categoryInstanceCreateRequest, deserializationErrors) = + await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken: cancellationToken); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + return await CreateCategoryInstance( + categoryJson, + categoryInstanceCreateRequest!.MachineName, + categoryInstanceCreateRequest!.CategoryConfigurationId, + categoryInstanceCreateRequest.CategoryTreeId, + categoryInstanceCreateRequest.ParentId, + categoryInstanceCreateRequest.TenantId, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names. + /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// id of entity configuration which has all category attributes + /// id of category tree, which represents separated hirerarchy with relations between categories + /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. + /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + JsonElement categoryJson, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) + = await DeserializeCategoryInstanceCreateRequestFromJson( + categoryJson, + machineName, + categoryConfigurationId, + categoryTreeId, + parentId, + tenantId, + cancellationToken + ); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + if (requestDeserializedCallback != null) + { + categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!); + } + + var (createdCategory, validationErrors) = await CreateCategoryInstance( + categoryInstanceCreateRequest!, cancellationToken + ); + + if (validationErrors != null) + { + return (null, validationErrors); + } + + return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null); + } + + public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance( + CategoryInstanceCreateRequest categoryCreateRequest, + CancellationToken cancellationToken = default + ) + { + CategoryTree? tree = await _categoryTreeRepository.LoadAsync( + categoryCreateRequest.CategoryTreeId, + categoryCreateRequest.CategoryTreeId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + if (tree == null) + { + return (null, new ValidationErrorResponse("CategoryTreeId", "Category tree not found"))!; + } + + if (tree.EntityConfigurationId != categoryCreateRequest.CategoryConfigurationId) + { + return (null, + new ValidationErrorResponse("CategoryConfigurationId", + "Category tree uses another configuration for categories" + ))!; + } + + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + categoryCreateRequest.CategoryConfigurationId, + categoryCreateRequest.CategoryConfigurationId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("CategoryConfigurationId", "Configuration not found"))!; + } + + List attributeConfigurations = + await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ).ConfigureAwait(false); + + + (var categoryPath, Guid? parentId, ProblemDetails? errors) = + await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); + + if (errors != null) + { + return (null, errors)!; + } + + var categoryInstance = new Category( + Guid.NewGuid(), + categoryCreateRequest.MachineName, + categoryCreateRequest.CategoryConfigurationId, + _mapper.Map>(categoryCreateRequest.Attributes), + categoryCreateRequest.TenantId, + categoryPath!, + parentId, + categoryCreateRequest.CategoryTreeId + ); + + var validationErrors = new Dictionary(); + foreach (AttributeConfiguration a in attributeConfigurations) + { + AttributeInstance? attributeValue = categoryInstance.Attributes + .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); + + List attrValidationErrors = a.ValidateInstance(attributeValue); + if (attrValidationErrors is { Count: > 0 }) + { + validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); + } + } + + if (validationErrors.Count > 0) + { + return (null, new ValidationErrorResponse(validationErrors))!; + } + + + + var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken) + .ConfigureAwait(false); + if (!saved) + { + //TODO: What do we want to do with internal exceptions and unsuccessful flow? + throw new Exception("Entity was not saved"); + } + + return (_mapper.Map(categoryInstance), null)!; + } + + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", + /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, + /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories + /// "parentId" - id guid of category from which new branch of hierarchy will be built. + /// Can be null if placed at the root of category tree. + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( + JsonElement categoryJson, + CancellationToken cancellationToken = default + ) + { + Guid categoryConfigurationId; + if (categoryJson.TryGetProperty("categoryConfigurationId", out var categoryConfigurationIdJsonElement)) + { + if (categoryConfigurationIdJsonElement.TryGetGuid(out var categoryConfigurationIdGuid)) + { + categoryConfigurationId = categoryConfigurationIdGuid; + } + else + { + return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is missing")); + } + + Guid categoryTreeId; + if (categoryJson.TryGetProperty("categoryTreeId", out var categoryTreeIdJsonElement)) + { + if (categoryTreeIdJsonElement.TryGetGuid(out var categoryTreeIdGuid)) + { + categoryTreeId = categoryTreeIdGuid; + } + else + { + return (null, new ValidationErrorResponse("categoryTreeId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("categoryTreeId", "Value is missing")); + } + + Guid? parentId = null; + if (categoryJson.TryGetProperty("parentId", out var parentIdJsonElement)) + { + if (parentIdJsonElement.ValueKind == JsonValueKind.Null) + { + parentId = null; + } + else if (parentIdJsonElement.TryGetGuid(out var parentIdGuid)) + { + parentId = parentIdGuid; + } + else + { + return (null, new ValidationErrorResponse("parentId", "Value is not a valid Guid"))!; + } + } + + Guid? tenantId = null; + if (categoryJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) + { + if (tenantIdJsonElement.ValueKind == JsonValueKind.Null) + { + tenantId = null; + } + else if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) + { + tenantId = tenantIdGuid; + } + else + { + return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; + } + } + + string? machineName = null; + if (categoryJson.TryGetProperty("machineName", out var machineNameJsonElement)) + { + machineName = machineNameJsonElement.ValueKind == JsonValueKind.Null ? null : machineNameJsonElement.GetString(); + if (machineName == null) + { + return (null, new ValidationErrorResponse("machineName", "Value is not a valid")); + } + } + + return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, machineName!, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); + } + + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names. + /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( + JsonElement categoryJson, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? categoryConfiguration = await _entityConfigurationRepository.LoadAsync( + categoryConfigurationId, + categoryConfigurationId.ToString(), + cancellationToken + ) + .ConfigureAwait(false); + + if (categoryConfiguration == null) + { + return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; + } + + List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( + categoryConfiguration, + cancellationToken + ) + .ConfigureAwait(false); + + return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( + categoryConfigurationId, machineName, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson + ); + } + + /// + /// Returns full category tree. + /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same lavel as a provided. + /// + /// + /// + /// + + [SuppressMessage("Performance", "CA1806:Do not ignore method results")] + public async Task> GetCategoryTreeViewAsync( + Guid treeId, + Guid? notDeeperThanCategoryId = null, + CancellationToken cancellationToken = default + ) + { + CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) + .ConfigureAwait(false); + if (tree == null) + { + throw new NotFoundException("Category tree not found"); + } + + ProjectionQueryResult treeElementsQueryResult = + await QueryInstances(tree.EntityConfigurationId, + new ProjectionQuery + { + Filters = new List { new("CategoryPaths.TreeId", FilterOperator.Equal, treeId) }, + Limit = _elasticSearchQueryOptions.MaxSize + }, + cancellationToken + ).ConfigureAwait(false); + + var treeElements = treeElementsQueryResult.Records + .Select(x => x.Document!) + .Select(x => + { + x.CategoryPaths = x.CategoryPaths.Where(cp => cp.TreeId == treeId).ToList(); + return x; + }).ToList(); + + + return BuildTreeView(treeElements, notDeeperThanCategoryId); + + } + + private List BuildTreeView(List categories, Guid? notDeeperThanCategoryId) + { + + int searchedLevelPathLenght; + + if (notDeeperThanCategoryId != null) + { + var category = categories.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); + + if (category == null) + { + throw new NotFoundException("Category not found"); + } + + searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault()!.Path.Length; + + categories = categories + .Where(x => x.CategoryPaths.FirstOrDefault()!.Path.Length <= searchedLevelPathLenght).ToList(); + } + + var treeViewModel = new List(); + + // Go through each instance once + foreach (CategoryViewModel treeElement in categories + .OrderBy(x => x.CategoryPaths.FirstOrDefault()?.Path.Length)) + { + var treeElementViewModel = _mapper.Map(treeElement); + var categoryPath = treeElement.CategoryPaths.FirstOrDefault()?.Path; + + // If categoryPath is empty, that this is a root model -> add it directly to the tree + if (string.IsNullOrEmpty(categoryPath)) + { + treeViewModel.Add(treeElementViewModel); + } + else + { + // Else split categoryPath and extract each parent machine name + IEnumerable categoryPathElements = + categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x)); + + // Go through each element of the path, remembering where we are atm, and passing current version of treeViewModel + // Applies an accumulator function over a sequence of paths. + EntityTreeInstanceViewModel? currentLevel = null; + + categoryPathElements.Aggregate( + treeViewModel, // initial value + (treeViewModelCurrent, pathComponent) => // apply function to a sequence + { + // try to find parent with current pathComponent in the current version of treeViewModel in case + // it had already been added to our tree model on previous iterations + EntityTreeInstanceViewModel? parent = + treeViewModelCurrent.FirstOrDefault(y => y.MachineName == pathComponent); + + // If it is not still there -> find it in the global list of categories and add to our treeViewModel + if (parent == null) + { + CategoryViewModel? parentInstance = categories.FirstOrDefault(y => y.MachineName == pathComponent); + parent = _mapper.Map(parentInstance); + treeViewModelCurrent.Add(parent); + } + + // Move to the next level + currentLevel = parent; + return parent.Children; + } + ); + currentLevel?.Children.Add(treeElementViewModel); + } + } + return treeViewModel; + + } + + /// + /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format. + /// + /// + /// + /// + public async Task> GetSubcategories( + Guid categoryTreeId, + Guid? parentId = null, + string? parentMachineName = null, + CancellationToken cancellationToken = default + ) + { + var categoryTree = await _categoryTreeRepository.LoadAsync( + categoryTreeId, categoryTreeId.ToString(), cancellationToken + ).ConfigureAwait(false); + + if (categoryTree == null) + { + throw new NotFoundException("Category tree not found"); + } + + var query = GetSubcategoriesPrepareQuery(categoryTree, parentId, parentMachineName, cancellationToken); + + var queryResult = _mapper.Map>( + await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) + ); + + return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); + } + + private ProjectionQuery GetSubcategoriesPrepareQuery( + CategoryTree categoryTree, + Guid? parentId, + string? parentMachineName, + CancellationToken cancellationToken = default + ) + { + ProjectionQuery query = new ProjectionQuery + { + Limit = _elasticSearchQueryOptions.MaxSize + }; + + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", + Operator = FilterOperator.Equal, + Value = categoryTree.Id.ToString(), + }); + + // If nothing is set - get subcategories of master level + if (parentId == null && string.IsNullOrEmpty(parentMachineName)) + { + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + Operator = FilterOperator.Equal, + Value = string.Empty, + }); + return query; + } + + if (parentId != null) + { + + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}", + Operator = FilterOperator.Equal, + Value = parentId.ToString() + }); + } + + if (!string.IsNullOrEmpty(parentMachineName)) + { + + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + Operator = FilterOperator.Equal, + Value = parentMachineName + }); + } + return query; + + } + + #endregion + +} diff --git a/CloudFabric.EAV.Service/EAVEntityInstanceService.cs b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs new file mode 100644 index 0000000..2ceab7c --- /dev/null +++ b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs @@ -0,0 +1,477 @@ +using System.Text.Json; + +using AutoMapper; + +using CloudFabric.EAV.Domain.Models; +using CloudFabric.EAV.Domain.Models.Attributes; +using CloudFabric.EAV.Enums; +using CloudFabric.EAV.Models.RequestModels; +using CloudFabric.EAV.Models.ViewModels; +using CloudFabric.EAV.Options; +using CloudFabric.EAV.Service.Serialization; +using CloudFabric.EventSourcing.Domain; +using CloudFabric.EventSourcing.EventStore; +using CloudFabric.EventSourcing.EventStore.Persistence; +using CloudFabric.Projections; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using ProjectionDocumentSchemaFactory = + CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; +namespace CloudFabric.EAV.Service; + +public class EAVEntityInstanceService: EAVService +{ + + public EAVEntityInstanceService(ILogger> logger, + IMapper mapper, + JsonSerializerOptions jsonSerializerOptions, + AggregateRepositoryFactory aggregateRepositoryFactory, + ProjectionRepositoryFactory projectionRepositoryFactory, + EventUserInfo userInfo) : base(logger, + new EntityInstanceFromDictionaryDeserializer(mapper), + mapper, + jsonSerializerOptions, + aggregateRepositoryFactory, + projectionRepositoryFactory, + userInfo) + { + } + + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "sku" and "name" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( + JsonElement entityJson, + CancellationToken cancellationToken = default + ) + { + Guid entityConfigurationId; + if (entityJson.TryGetProperty("entityConfigurationId", out var entityConfigurationIdJsonElement)) + { + if (entityConfigurationIdJsonElement.TryGetGuid(out var entityConfigurationIdGuid)) + { + entityConfigurationId = entityConfigurationIdGuid; + } + else + { + return (null, new ValidationErrorResponse("entityConfigurationId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("entityConfigurationId", "Value is missing")); + } + + Guid tenantId; + if (entityJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) + { + if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) + { + tenantId = tenantIdGuid; + } + else + { + return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("tenantId", "Value is missing")); + } + + return await DeserializeEntityInstanceCreateRequestFromJson( + entityJson, entityConfigurationId, tenantId, cancellationToken + ); + } + + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity" + /// } + /// ``` + /// + /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( + JsonElement entityJson, + Guid entityConfigurationId, + Guid tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + entityConfigurationId, + entityConfigurationId.ToString(), + cancellationToken + ) + .ConfigureAwait(false); + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "EntityConfiguration not found"))!; + } + + List attributeConfigurations = + await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ) + .ConfigureAwait(false); + + return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeEntityInstanceCreateRequest( + entityConfigurationId, tenantId, attributeConfigurations, entityJson + ); + } + + /// + /// Create new entity instance from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "sku" and "name" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + string entityJsonString, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + JsonDocument entityJson = JsonDocument.Parse(entityJsonString); + + return CreateEntityInstance( + entityJson.RootElement, + requestDeserializedCallback, + dryRun, + requiredAttributesCanBeNull, + cancellationToken + ); + } + + /// + /// Create new entity instance from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity" + /// } + /// ``` + /// + /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// Id of entity configuration which has all attributes + /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + string entityJsonString, + Guid entityConfigurationId, + Guid tenantId, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + JsonDocument entityJson = JsonDocument.Parse(entityJsonString); + + return CreateEntityInstance( + entityJson.RootElement, + entityConfigurationId, + tenantId, + requestDeserializedCallback, + dryRun, + requiredAttributesCanBeNull, + cancellationToken + ); + } + + /// + /// Create new entity instance from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "sku" and "name" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + JsonElement entityJson, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + var (entityInstanceCreateRequest, deserializationErrors) = + await DeserializeEntityInstanceCreateRequestFromJson(entityJson, cancellationToken); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + return await CreateEntityInstance( + entityJson, + // Deserialization method ensures that EntityConfigurationId and TenantId exist and returns errors if not + // so it's safe to use ! here + entityInstanceCreateRequest!.EntityConfigurationId, + entityInstanceCreateRequest.TenantId!.Value, + requestDeserializedCallback, + dryRun, + requiredAttributesCanBeNull, + cancellationToken + ); + } + + /// + /// Create new entity instance from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity" + /// } + /// ``` + /// + /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// Id of entity configuration which has all attributes + /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + JsonElement entityJson, + Guid entityConfigurationId, + Guid tenantId, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + var (entityInstanceCreateRequest, deserializationErrors) = await + DeserializeEntityInstanceCreateRequestFromJson( + entityJson, entityConfigurationId, tenantId, cancellationToken + ); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + if (requestDeserializedCallback != null) + { + entityInstanceCreateRequest = await requestDeserializedCallback(entityInstanceCreateRequest!, dryRun); + } + + var (createdEntity, validationErrors) = await CreateEntityInstance( + entityInstanceCreateRequest!, dryRun, requiredAttributesCanBeNull, cancellationToken + ); + + if (validationErrors != null) + { + return (null, validationErrors); + } + + return (SerializeEntityInstanceToJsonMultiLanguage(createdEntity), null); + } + + + + public async Task<(EntityInstanceViewModel?, ProblemDetails?)> CreateEntityInstance( + EntityInstanceCreateRequest entity, bool dryRun = false, bool requiredAttributesCanBeNull = false, CancellationToken cancellationToken = default + ) + { + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + entity.EntityConfigurationId, + entity.EntityConfigurationId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; + } + + List attributeConfigurations = + await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ).ConfigureAwait(false); + + //TODO: add check for categoryPath + var entityInstance = new EntityInstance( + Guid.NewGuid(), + entity.EntityConfigurationId, + _mapper.Map>(entity.Attributes), + entity.TenantId + ); + + var validationErrors = new Dictionary(); + foreach (AttributeConfiguration a in attributeConfigurations) + { + AttributeInstance? attributeValue = entityInstance.Attributes + .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); + + List attrValidationErrors = a.ValidateInstance(attributeValue, requiredAttributesCanBeNull); + if (attrValidationErrors is { Count: > 0 }) + { + validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); + } + + // Note that this method updates entityConfiguration state (for serial attribute it increments the number + // stored in externalvalues) but does not save entity configuration, we need to do that manually outside of + // the loop + InitializeAttributeInstanceWithExternalValuesFromEntity(entityConfiguration, a, attributeValue); + } + + if (validationErrors.Count > 0) + { + return (null, new ValidationErrorResponse(validationErrors))!; + } + + if (!dryRun) + { + var entityConfigurationSaved = await _entityConfigurationRepository + .SaveAsync(_userInfo, entityConfiguration, cancellationToken) + .ConfigureAwait(false); + + if (!entityConfigurationSaved) + { + throw new Exception("Entity was not saved"); + } + + ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory + .FromEntityConfiguration(entityConfiguration, attributeConfigurations); + + IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); + await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); + + var entityInstanceSaved = + await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, cancellationToken); + + if (!entityInstanceSaved) + { + //TODO: What do we want to do with internal exceptions and unsuccessful flow? + throw new Exception("Entity was not saved"); + } + + return (_mapper.Map(entityInstance), null); + } + + return (_mapper.Map(entityInstance), null); + } +} diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs index 779146b..a1fbbb0 100644 --- a/CloudFabric.EAV.Service/EAVService.cs +++ b/CloudFabric.EAV.Service/EAVService.cs @@ -15,7 +15,6 @@ using CloudFabric.EAV.Models.RequestModels.Attributes; using CloudFabric.EAV.Models.ViewModels; using CloudFabric.EAV.Models.ViewModels.Attributes; -using CloudFabric.EAV.Options; using CloudFabric.EAV.Service.Serialization; using CloudFabric.EventSourcing.Domain; using CloudFabric.EventSourcing.EventStore; @@ -25,85 +24,86 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using ProjectionDocumentSchemaFactory = CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; namespace CloudFabric.EAV.Service; -public class EAVService : IEAVService +[SuppressMessage("ReSharper", "InconsistentNaming")] +public abstract class EAVService where TViewModel: EntityInstanceViewModel + where TUpdateRequest: EntityInstanceUpdateRequest + where TEntityType: EntityInstanceBase { - private readonly AggregateRepositoryFactory _aggregateRepositoryFactory; - private readonly IProjectionRepository _attributeConfigurationProjectionRepository; private readonly AggregateRepository _attributeConfigurationRepository; - private readonly AggregateRepository _categoryTreeRepository; private readonly IProjectionRepository _entityConfigurationProjectionRepository; - private readonly AggregateRepository _entityConfigurationRepository; + internal readonly AggregateRepository _entityConfigurationRepository; - private readonly EntityInstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer; + private readonly InstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer; - private readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer + internal readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer _entityInstanceCreateUpdateRequestFromJsonDeserializer; - private readonly AggregateRepository _entityInstanceRepository; - private readonly ILogger _logger; - private readonly IMapper _mapper; + internal readonly AggregateRepository _entityInstanceRepository; + private readonly ILogger> _logger; + internal readonly IMapper _mapper; private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + internal readonly ProjectionRepositoryFactory _projectionRepositoryFactory; - private readonly EventUserInfo _userInfo; + internal readonly EventUserInfo _userInfo; - private readonly ElasticSearchQueryOptions _elasticSearchQueryOptions; + internal readonly AggregateRepository _categoryTreeRepository; + internal readonly AggregateRepository _categoryInstanceRepository; - public EAVService( - ILogger logger, + protected EAVService( + ILogger> logger, + InstanceFromDictionaryDeserializer instanceFromDictionaryDeserializer, IMapper mapper, JsonSerializerOptions jsonSerializerOptions, AggregateRepositoryFactory aggregateRepositoryFactory, ProjectionRepositoryFactory projectionRepositoryFactory, - EventUserInfo userInfo, - IOptions? elasticSearchQueryOptions = null + EventUserInfo userInfo ) { _logger = logger; _mapper = mapper; _jsonSerializerOptions = jsonSerializerOptions; - _aggregateRepositoryFactory = aggregateRepositoryFactory; _projectionRepositoryFactory = projectionRepositoryFactory; _userInfo = userInfo; - _elasticSearchQueryOptions = elasticSearchQueryOptions != null - ? elasticSearchQueryOptions.Value - : new ElasticSearchQueryOptions(); - _attributeConfigurationRepository = _aggregateRepositoryFactory + + _attributeConfigurationRepository = aggregateRepositoryFactory .GetAggregateRepository(); - _entityConfigurationRepository = _aggregateRepositoryFactory + _entityConfigurationRepository = aggregateRepositoryFactory .GetAggregateRepository(); - _entityInstanceRepository = _aggregateRepositoryFactory - .GetAggregateRepository(); - _categoryTreeRepository = _aggregateRepositoryFactory - .GetAggregateRepository(); + _entityInstanceRepository = aggregateRepositoryFactory + .GetAggregateRepository(); + _attributeConfigurationProjectionRepository = _projectionRepositoryFactory .GetProjectionRepository(); _entityConfigurationProjectionRepository = _projectionRepositoryFactory .GetProjectionRepository(); - _entityInstanceFromDictionaryDeserializer = new EntityInstanceFromDictionaryDeserializer(_mapper); + _entityInstanceFromDictionaryDeserializer = instanceFromDictionaryDeserializer; _entityInstanceCreateUpdateRequestFromJsonDeserializer = new EntityInstanceCreateUpdateRequestFromJsonDeserializer( _attributeConfigurationRepository, jsonSerializerOptions ); + + _categoryInstanceRepository = aggregateRepositoryFactory + .GetAggregateRepository(); + _categoryTreeRepository = aggregateRepositoryFactory + .GetAggregateRepository(); } private void EnsureAttributeMachineNameIsAdded(AttributeConfigurationCreateUpdateRequest attributeRequest) @@ -157,7 +157,7 @@ private async Task CheckAttributesListMachineNameUnique( .Select(x => ((EntityAttributeConfigurationCreateUpdateReferenceRequest)x).AttributeConfigurationId) .ToList(); - List machineNames = new(); + List machineNames = new List(); if (referenceAttributes.Any()) { machineNames = (await GetAttributesByIds(referenceAttributes, cancellationToken)) @@ -185,61 +185,7 @@ private async Task CheckAttributesListMachineNameUnique( return true; } - private void InitializeAttributeInstanceWithExternalValuesFromEntity( - EntityConfiguration entityConfiguration, - AttributeConfiguration attributeConfiguration, - AttributeInstance? attributeInstance - ) - { - switch (attributeConfiguration.ValueType) - { - case EavAttributeType.Serial: - { - if (attributeInstance == null) - { - return; - } - - var serialAttributeConfiguration = attributeConfiguration as SerialAttributeConfiguration; - - var serialInstance = attributeInstance as SerialAttributeInstance; - - if (serialAttributeConfiguration == null || serialInstance == null) - { - throw new ArgumentException("Invalid attribute type"); - } - - EntityConfigurationAttributeReference? entityAttribute = entityConfiguration.Attributes - .FirstOrDefault(x => x.AttributeConfigurationId == attributeConfiguration.Id); - - if (entityAttribute == null) - { - throw new NotFoundException("Attribute not found"); - } - - var existingAttributeValue = - entityAttribute.AttributeConfigurationExternalValues.FirstOrDefault(); - - long? deserializedValue = null; - - if (existingAttributeValue != null) - { - deserializedValue = JsonSerializer.Deserialize(existingAttributeValue.ToString()!); - } - - var newExternalValue = existingAttributeValue == null - ? serialAttributeConfiguration.StartingNumber - : deserializedValue += serialAttributeConfiguration.Increment; - - serialInstance.Value = newExternalValue!.Value; - entityConfiguration.UpdateAttrributeExternalValues(attributeConfiguration.Id, - new List { newExternalValue } - ); - } - break; - } - } /// /// Update entity configuration external values. @@ -330,10 +276,9 @@ private async Task attributesIds, CancellationToken cancellationToken) { // create attributes filter - Filter attributeIdFilter = new(nameof(AttributeConfigurationProjectionDocument.Id), + Filter attributeIdFilter = new Filter(nameof(AttributeConfigurationProjectionDocument.Id), FilterOperator.Equal, - attributesIds[0] - ); + attributesIds[0]); foreach (Guid attributesId in attributesIds.Skip(1)) { @@ -356,31 +301,31 @@ private async Task BuildCategoryPath(Guid treeId, Guid? parentId, + internal async Task<(string?, Guid?, ProblemDetails?)> BuildCategoryPath(Guid treeId, Guid? parentId, CancellationToken cancellationToken) { CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) .ConfigureAwait(false); if (tree == null) { - return (null, new ValidationErrorResponse("TreeId", "Tree not found"))!; + return (null, null, new ValidationErrorResponse("TreeId", "Tree not found")); } Category? parent = parentId == null ? null - : _mapper.Map(await _entityInstanceRepository + : _mapper.Map(await _categoryInstanceRepository .LoadAsync(parentId.Value, tree.EntityConfigurationId.ToString(), cancellationToken) .ConfigureAwait(false) ); if (parent == null && parentId != null) { - return (null, new ValidationErrorResponse("ParentId", "Parent category not found"))!; + return (null, null, new ValidationErrorResponse("ParentId", "Parent category not found")); } CategoryPath? parentPath = parent?.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId); - var categoryPath = parentPath == null ? "" : $"{parentPath.Path}/{parent?.Id}"; - return (categoryPath, null)!; + var categoryPath = parentPath == null ? "" : $"{parentPath.Path}/{parent?.MachineName}"; + return (categoryPath, parent?.Id, null); } #region EntityConfiguration @@ -561,7 +506,7 @@ CancellationToken cancellationToken nameof(entityConfigurationCreateRequest.Attributes), "Attributes machine name must be unique" ) - )!; + ); } foreach (EntityAttributeConfigurationCreateUpdateRequest attribute in entityConfigurationCreateRequest @@ -585,7 +530,7 @@ CancellationToken cancellationToken nameof(entityConfigurationCreateRequest.Attributes), "One or more attribute not found" ) - )!; + ); } } @@ -645,7 +590,6 @@ await CreateAttribute( new ValidationErrorResponse(entityConfiguration.MachineName, entityValidationErrors.ToArray())); } - await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); await _entityConfigurationRepository.SaveAsync( _userInfo, @@ -653,7 +597,9 @@ await _entityConfigurationRepository.SaveAsync( cancellationToken ).ConfigureAwait(false); - return (_mapper.Map(entityConfiguration), null)!; + await EnsureProjectionIndexForEntityConfiguration(entityConfiguration); + + return (_mapper.Map(entityConfiguration), null); } public async Task<(EntityConfigurationViewModel?, ProblemDetails?)> UpdateEntityConfiguration( @@ -677,7 +623,7 @@ await _entityConfigurationRepository.SaveAsync( new ValidationErrorResponse(nameof(entityUpdateRequest.Attributes), "Attributes machine name must be unique" ) - )!; + ); } EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( @@ -689,7 +635,7 @@ await _entityConfigurationRepository.SaveAsync( if (entityConfiguration == null) { return (null, - new ValidationErrorResponse(nameof(entityUpdateRequest.Id), "Entity configuration not found"))!; + new ValidationErrorResponse(nameof(entityUpdateRequest.Id), "Entity configuration not found")); } var entityConfigurationExistingAttributes = @@ -723,7 +669,10 @@ await _entityConfigurationRepository.SaveAsync( cancellationToken ).ConfigureAwait(false); - if (attributeConfiguration != null && !attributeConfiguration.IsDeleted) + if (attributeConfiguration is + { + IsDeleted: false + }) { if (attributeShouldBeAdded) { @@ -798,11 +747,26 @@ await CreateAttribute( new ValidationErrorResponse(entityConfiguration.MachineName, entityValidationErrors.ToArray())); } - await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); + //await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) .ConfigureAwait(false); - return (_mapper.Map(entityConfiguration), null)!; + await EnsureProjectionIndexForEntityConfiguration(entityConfiguration); + + return (_mapper.Map(entityConfiguration), null); + } + + private async Task EnsureProjectionIndexForEntityConfiguration(EntityConfiguration entityConfiguration) + { + // when entity configuration is created or updated, we need to create a projections index for it. EnsureIndex + // will just create a record that such index is needed. Then, it will be picked up by background processor + List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration + ); + var schema = ProjectionDocumentSchemaFactory + .FromEntityConfiguration(entityConfiguration, attributeConfigurations); + IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); + await projectionRepository.EnsureIndex(); } public async Task<(EntityConfigurationViewModel?, ProblemDetails?)> AddAttributeToEntityConfiguration( @@ -831,7 +795,7 @@ await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, c if (entityConfiguration.Attributes.Any(x => x.AttributeConfigurationId == attributeConfiguration.Id)) { - return (null, new ValidationErrorResponse(nameof(attributeId), "Attribute has already been added"))!; + return (null, new ValidationErrorResponse(nameof(attributeId), "Attribute has already been added")); } if (!await IsAttributeMachineNameUniqueForEntityConfiguration( @@ -851,7 +815,7 @@ await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, c await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) .ConfigureAwait(false); - return (_mapper.Map(entityConfiguration), null)!; + return (_mapper.Map(entityConfiguration), null); } public async Task<(AttributeConfigurationViewModel, ProblemDetails)> CreateAttributeAndAddToEntityConfiguration( @@ -934,7 +898,10 @@ public async Task DeleteAttributes(List attributesIds, CancellationToken c cancellationToken ); - if (attributeConfiguration != null && !attributeConfiguration.IsDeleted) + if (attributeConfiguration is + { + IsDeleted: false + }) { attributeConfiguration.Delete(); @@ -950,11 +917,7 @@ await _attributeConfigurationRepository ) ); - Filter filters = new( - filterPropertyName, - FilterOperator.Equal, - attributesIds[0] - ); + Filter filters = new Filter(filterPropertyName, FilterOperator.Equal, attributesIds[0]); foreach (Guid attributeId in attributesIds.Skip(1)) { @@ -1078,1268 +1041,122 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR // return _mapper.Map>(records); // } - #region Categories + #region BaseEntityInstance - public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync( - CategoryTreeCreateRequest entity, - Guid? tenantId, - CancellationToken cancellationToken = default - ) + public async Task GetEntityInstance(Guid id, string partitionKey) { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entity.EntityConfigurationId, - entity.EntityConfigurationId.ToString(), - cancellationToken - ).ConfigureAwait(false); - - if (entityConfiguration == null) - { - return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; - } - - var tree = new CategoryTree( - Guid.NewGuid(), - entity.EntityConfigurationId, - entity.MachineName, - tenantId - ); + TEntityType? entityInstance = await _entityInstanceRepository.LoadAsync(id, partitionKey); - _ = await _categoryTreeRepository.SaveAsync(_userInfo, tree, cancellationToken).ConfigureAwait(false); - return (_mapper.Map(tree), null)!; + return _mapper.Map(entityInstance); } - /// - /// Create new category from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", - /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, - /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories - /// "parentId" - id guid of category from which new branch of hierarchy will be built. - /// Can be null if placed at the root of category tree. - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - string categoryJsonString, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default - ) + public async Task GetEntityInstanceJsonMultiLanguage(Guid id, string partitionKey) { - JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); + TViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); - return CreateCategoryInstance( - categoryJson.RootElement, - requestDeserializedCallback, - cancellationToken - ); + return SerializeEntityInstanceToJsonMultiLanguage(entityInstanceViewModel); } - /// - /// Create new category from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names. - /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// id of entity configuration which has all category attributes - /// id of category tree, which represents separated hirerarchy with relations between categories - /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. - /// tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - string categoryJsonString, - Guid categoryConfigurationId, - Guid categoryTreeId, - Guid? parentId, - Guid? tenantId, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default - ) + public async Task GetEntityInstanceJsonSingleLanguage( + Guid id, + string partitionKey, + string language, + string fallbackLanguage = "en-US") { - JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); - - return CreateCategoryInstance( - categoryJson.RootElement, - categoryConfigurationId, - categoryTreeId, - parentId, - tenantId, - requestDeserializedCallback, - cancellationToken - ); + TViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); + + return SerializeEntityInstanceToJsonSingleLanguage(entityInstanceViewModel, language, fallbackLanguage); } - /// - /// Create new category from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", - /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, - /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories - /// "parentId" - id guid of category from which new branch of hierarchy will be built. - /// Can be null if placed at the root of category tree. - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - JsonElement categoryJson, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default - ) + protected JsonDocument SerializeEntityInstanceToJsonMultiLanguage(TViewModel? entityInstanceViewModel) { - var (categoryInstanceCreateRequest, deserializationErrors) = - await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } + var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); + serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); + serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); - return await CreateCategoryInstance( - categoryJson, - categoryInstanceCreateRequest!.CategoryConfigurationId, - categoryInstanceCreateRequest.CategoryTreeId, - categoryInstanceCreateRequest.ParentId, - categoryInstanceCreateRequest.TenantId, - requestDeserializedCallback, - cancellationToken - ); + return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); } - /// - /// Create new category from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names. - /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// id of entity configuration which has all category attributes - /// id of category tree, which represents separated hirerarchy with relations between categories - /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. - /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - JsonElement categoryJson, - Guid categoryConfigurationId, - Guid categoryTreeId, - Guid? parentId, - Guid? tenantId, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default + private JsonDocument SerializeEntityInstanceToJsonSingleLanguage( + TViewModel? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" ) { - (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) - = await DeserializeCategoryInstanceCreateRequestFromJson( - categoryJson, - categoryConfigurationId, - categoryTreeId, - parentId, - tenantId, - cancellationToken - ); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } - - if (requestDeserializedCallback != null) - { - categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!); - } - - var (createdCategory, validationErrors) = await CreateCategoryInstance( - categoryInstanceCreateRequest!, cancellationToken - ); - - if (validationErrors != null) - { - return (null, validationErrors); - } + var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); + serializerOptions.Converters.Add(new LocalizedStringSingleLanguageSerializer(language, fallbackLanguage)); + serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); - return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null); + return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); } - public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance( - CategoryInstanceCreateRequest categoryCreateRequest, + public async Task<(TViewModel, ProblemDetails)> UpdateEntityInstance( + string partitionKey, + TUpdateRequest updateRequest, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, CancellationToken cancellationToken = default ) { - CategoryTree? tree = await _categoryTreeRepository.LoadAsync( - categoryCreateRequest.CategoryTreeId, - categoryCreateRequest.CategoryTreeId.ToString(), - cancellationToken - ).ConfigureAwait(false); - - if (tree == null) - { - return (null, new ValidationErrorResponse("CategoryTreeId", "Category tree not found"))!; - } + TEntityType? entityInstance = + await _entityInstanceRepository.LoadAsync(updateRequest.Id, partitionKey, cancellationToken); - if (tree.EntityConfigurationId != categoryCreateRequest.CategoryConfigurationId) + if (entityInstance == null) { - return (null, - new ValidationErrorResponse("CategoryConfigurationId", - "Category tree uses another configuration for categories" - ))!; + return (null, new ValidationErrorResponse(nameof(updateRequest.Id), "Entity instance not found"))!; } EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - categoryCreateRequest.CategoryConfigurationId, - categoryCreateRequest.CategoryConfigurationId.ToString(), + entityInstance.EntityConfigurationId, + entityInstance.EntityConfigurationId.ToString(), cancellationToken - ).ConfigureAwait(false); - + ); if (entityConfiguration == null) { - return (null, new ValidationErrorResponse("CategoryConfigurationId", "Configuration not found"))!; + return (null, + new ValidationErrorResponse(nameof(updateRequest.EntityConfigurationId), + "Entity configuration not found" + ))!; } - List attributeConfigurations = + List entityConfigurationAttributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( entityConfiguration, cancellationToken - ).ConfigureAwait(false); - + ); - (var categoryPath, ProblemDetails? errors) = - await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); + var validationErrors = new Dictionary(); - if (errors != null) + if (updateRequest.AttributeMachineNamesToRemove != null) { - return (null, errors)!; - } + foreach (var attributeMachineNameToRemove in updateRequest.AttributeMachineNamesToRemove) + { + AttributeConfiguration? attrConfiguration = entityConfigurationAttributeConfigurations + .First(c => c.MachineName == attributeMachineNameToRemove); + updateRequest.AttributesToAddOrUpdate.RemoveAll(a => + a.ConfigurationAttributeMachineName == attributeMachineNameToRemove); - var categoryInstance = new Category( - Guid.NewGuid(), - categoryCreateRequest.CategoryConfigurationId, - _mapper.Map>(categoryCreateRequest.Attributes), - categoryCreateRequest.TenantId, - categoryPath!, - categoryCreateRequest.CategoryTreeId - ); + if (requiredAttributesCanBeNull) + { + entityInstance.RemoveAttributeInstance(attributeMachineNameToRemove); + continue; + } - var validationErrors = new Dictionary(); - foreach (AttributeConfiguration a in attributeConfigurations) - { - AttributeInstance? attributeValue = categoryInstance.Attributes - .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); + // validation against null will check if the attribute is required + List errors = attrConfiguration.ValidateInstance(null); - List attrValidationErrors = a.ValidateInstance(attributeValue); - if (attrValidationErrors is { Count: > 0 }) - { - validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); + if (errors.Count == 0) + { + entityInstance.RemoveAttributeInstance(attributeMachineNameToRemove); + } + else + { + validationErrors.Add(attributeMachineNameToRemove, errors.ToArray()); + } } } - if (validationErrors.Count > 0) - { - return (null, new ValidationErrorResponse(validationErrors))!; - } - - var mappedInstance = _mapper.Map(categoryInstance); - - ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory - .FromEntityConfiguration(entityConfiguration, attributeConfigurations); - - IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); - await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - - var saved = await _entityInstanceRepository.SaveAsync(_userInfo, mappedInstance, cancellationToken) - .ConfigureAwait(false); - if (!saved) - { - //TODO: What do we want to do with internal exceptions and unsuccessful flow? - throw new Exception("Entity was not saved"); - } - - return (_mapper.Map(categoryInstance), null)!; - } - - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", - /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, - /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories - /// "parentId" - id guid of category from which new branch of hierarchy will be built. - /// Can be null if placed at the root of category tree. - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( - JsonElement categoryJson, - CancellationToken cancellationToken = default - ) - { - Guid categoryConfigurationId; - if (categoryJson.TryGetProperty("categoryConfigurationId", out var categoryConfigurationIdJsonElement)) - { - if (categoryConfigurationIdJsonElement.TryGetGuid(out var categoryConfigurationIdGuid)) - { - categoryConfigurationId = categoryConfigurationIdGuid; - } - else - { - return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is missing")); - } - - Guid categoryTreeId; - if (categoryJson.TryGetProperty("categoryTreeId", out var categoryTreeIdJsonElement)) - { - if (categoryTreeIdJsonElement.TryGetGuid(out var categoryTreeIdGuid)) - { - categoryTreeId = categoryTreeIdGuid; - } - else - { - return (null, new ValidationErrorResponse("categoryTreeId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("categoryTreeId", "Value is missing")); - } - - Guid? parentId = null; - if (categoryJson.TryGetProperty("parentId", out var parentIdJsonElement)) - { - if (parentIdJsonElement.ValueKind == JsonValueKind.Null) - { - parentId = null; - } - else if (parentIdJsonElement.TryGetGuid(out var parentIdGuid)) - { - parentId = parentIdGuid; - } - else - { - return (null, new ValidationErrorResponse("parentId", "Value is not a valid Guid"))!; - } - } - - Guid? tenantId = null; - if (categoryJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) - { - if (tenantIdJsonElement.ValueKind == JsonValueKind.Null) - { - tenantId = null; - } - else if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) - { - tenantId = tenantIdGuid; - } - else - { - return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; - } - } - - return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); - } - - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names. - /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( - JsonElement categoryJson, - Guid categoryConfigurationId, - Guid categoryTreeId, - Guid? parentId, - Guid? tenantId, - CancellationToken cancellationToken = default - ) - { - EntityConfiguration? categoryConfiguration = await _entityConfigurationRepository.LoadAsync( - categoryConfigurationId, - categoryConfigurationId.ToString(), - cancellationToken - ) - .ConfigureAwait(false); - - if (categoryConfiguration == null) - { - return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; - } - - List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( - categoryConfiguration, - cancellationToken - ) - .ConfigureAwait(false); - - return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( - categoryConfigurationId, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson - ); - } - - /// - /// Returns full category tree. - /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same lavel as a provided. - /// - /// - /// - /// - - [SuppressMessage("Performance", "CA1806:Do not ignore method results")] - public async Task> GetCategoryTreeViewAsync( - Guid treeId, - Guid? notDeeperThanCategoryId = null, - CancellationToken cancellationToken = default - ) - { - CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) - .ConfigureAwait(false); - if (tree == null) - { - throw new NotFoundException("Category tree not found"); - } - - ProjectionQueryResult treeElementsQueryResult = - await QueryInstances(tree.EntityConfigurationId, - new ProjectionQuery - { - Filters = new List { new("CategoryPaths.TreeId", FilterOperator.Equal, treeId) }, - Limit = _elasticSearchQueryOptions.MaxSize - }, - cancellationToken - ).ConfigureAwait(false); - - var treeElements = treeElementsQueryResult.Records.Select(x => x.Document!).ToList(); - - int searchedLevelPathLenght; - - if (notDeeperThanCategoryId != null) - { - var category = treeElements.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); - - if (category == null) - { - throw new NotFoundException("Category not found"); - } - - searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length; - - treeElements = treeElements - .Where(x => x.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length <= searchedLevelPathLenght).ToList(); - } - - var treeViewModel = new List(); - - // Go through each instance once - foreach (EntityInstanceViewModel treeElement in treeElements - .OrderBy(x => x.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path.Length)) - { - var treeElementViewModel = _mapper.Map(treeElement); - var categoryPath = treeElement.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path; - - if (string.IsNullOrEmpty(categoryPath)) - { - treeViewModel.Add(treeElementViewModel); - } - else - { - IEnumerable categoryPathElements = - categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x)); - EntityTreeInstanceViewModel? currentLevel = null; - categoryPathElements.Aggregate(treeViewModel, - (acc, pathComponent) => - { - EntityTreeInstanceViewModel? parent = - acc.FirstOrDefault(y => y.Id.ToString() == pathComponent); - if (parent == null) - { - EntityInstanceViewModel? parentInstance = treeElements.FirstOrDefault(x => x.Id.ToString() == pathComponent); - parent = _mapper.Map(parentInstance); - acc.Add(parent); - } - - currentLevel = parent; - return parent.Children; - } - ); - currentLevel?.Children.Add(treeElementViewModel); - } - } - - return treeViewModel; - } - - /// - /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format. - /// - /// - /// - /// - public async Task> GetSubcategories( - Guid categoryTreeId, - Guid? parentId, - CancellationToken cancellationToken = default - ) - { - var categoryTree = await _categoryTreeRepository.LoadAsync( - categoryTreeId, categoryTreeId.ToString(), cancellationToken - ).ConfigureAwait(false); - - if (categoryTree == null) - { - throw new NotFoundException("Category tree not found"); - } - - var query = await GetSubcategoriesPrepareQuery(categoryTreeId, parentId, cancellationToken); - - var queryResult = _mapper.Map>( - await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) - ); - - return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); - } - - private async Task GetSubcategoriesPrepareQuery( - Guid categoryTreeId, - Guid? parentId, - CancellationToken cancellationToken = default - ) - { - var categoryTree = await _categoryTreeRepository.LoadAsync( - categoryTreeId, categoryTreeId.ToString(), cancellationToken - ).ConfigureAwait(false); - - if (categoryTree == null) - { - throw new NotFoundException("Category tree not found"); - } - - ProjectionQuery query = new ProjectionQuery - { - Limit = _elasticSearchQueryOptions.MaxSize - }; - - if (parentId == null) - { - query.Filters.AddRange( - - new List - { - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", - Operator = FilterOperator.Equal, - Value = categoryTree.Id.ToString(), - }, - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", - Operator = FilterOperator.Equal, - Value = string.Empty - } - } - ); - - return query; - } - - var category = await _entityInstanceRepository.LoadAsync( - parentId.Value, categoryTree.EntityConfigurationId.ToString(), cancellationToken - ).ConfigureAwait(false); - - if (category == null) - { - throw new NotFoundException("Category not found"); - } - - string categoryPath = category.CategoryPaths.Where(x => x.TreeId == categoryTree.Id) - .Select(p => p.Path).FirstOrDefault()!; - - query = new ProjectionQuery - { - Filters = new List - { - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", - Operator = FilterOperator.Equal, - Value = categoryTree.Id.ToString(), - }, - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", - Operator = FilterOperator.Equal, - Value = categoryPath + $"/{category.Id}" - } - } - }; - - return query; - } - - #endregion - - #region EntityInstance - - /// - /// Create new entity instance from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "sku" and "name" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - string entityJsonString, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - JsonDocument entityJson = JsonDocument.Parse(entityJsonString); - - return CreateEntityInstance( - entityJson.RootElement, - requestDeserializedCallback, - dryRun, - requiredAttributesCanBeNull, - cancellationToken - ); - } - - /// - /// Create new entity instance from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity" - /// } - /// ``` - /// - /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// Id of entity configuration which has all attributes - /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - string entityJsonString, - Guid entityConfigurationId, - Guid tenantId, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - JsonDocument entityJson = JsonDocument.Parse(entityJsonString); - - return CreateEntityInstance( - entityJson.RootElement, - entityConfigurationId, - tenantId, - requestDeserializedCallback, - dryRun, - requiredAttributesCanBeNull, - cancellationToken - ); - } - - /// - /// Create new entity instance from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "sku" and "name" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - JsonElement entityJson, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - var (entityInstanceCreateRequest, deserializationErrors) = - await DeserializeEntityInstanceCreateRequestFromJson(entityJson, cancellationToken); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } - - return await CreateEntityInstance( - entityJson, - // Deserialization method ensures that EntityConfigurationId and TenantId exist and returns errors if not - // so it's safe to use ! here - entityInstanceCreateRequest!.EntityConfigurationId, - entityInstanceCreateRequest.TenantId!.Value, - requestDeserializedCallback, - dryRun, - requiredAttributesCanBeNull, - cancellationToken - ); - } - - /// - /// Create new entity instance from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity" - /// } - /// ``` - /// - /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// Id of entity configuration which has all attributes - /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - JsonElement entityJson, - Guid entityConfigurationId, - Guid tenantId, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - var (entityInstanceCreateRequest, deserializationErrors) = await - DeserializeEntityInstanceCreateRequestFromJson( - entityJson, entityConfigurationId, tenantId, cancellationToken - ); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } - - if (requestDeserializedCallback != null) - { - entityInstanceCreateRequest = await requestDeserializedCallback(entityInstanceCreateRequest!, dryRun); - } - - var (createdEntity, validationErrors) = await CreateEntityInstance( - entityInstanceCreateRequest!, dryRun, requiredAttributesCanBeNull, cancellationToken - ); - - if (validationErrors != null) - { - return (null, validationErrors); - } - - return (SerializeEntityInstanceToJsonMultiLanguage(createdEntity), null); - } - - public async Task<(EntityInstanceViewModel?, ProblemDetails?)> CreateEntityInstance( - EntityInstanceCreateRequest entity, bool dryRun = false, bool requiredAttributesCanBeNull = false, CancellationToken cancellationToken = default - ) - { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entity.EntityConfigurationId, - entity.EntityConfigurationId.ToString(), - cancellationToken - ).ConfigureAwait(false); - - if (entityConfiguration == null) - { - return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; - } - - List attributeConfigurations = - await GetAttributeConfigurationsForEntityConfiguration( - entityConfiguration, - cancellationToken - ).ConfigureAwait(false); - - //TODO: add check for categoryPath - var entityInstance = new EntityInstance( - Guid.NewGuid(), - entity.EntityConfigurationId, - _mapper.Map>(entity.Attributes), - entity.TenantId - ); - - var validationErrors = new Dictionary(); - foreach (AttributeConfiguration a in attributeConfigurations) - { - AttributeInstance? attributeValue = entityInstance.Attributes - .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); - - List attrValidationErrors = a.ValidateInstance(attributeValue, requiredAttributesCanBeNull); - if (attrValidationErrors is { Count: > 0 }) - { - validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); - } - - // Note that this method updates entityConfiguration state (for serial attribute it increments the number - // stored in externalvalues) but does not save entity configuration, we need to do that manually outside of - // the loop - InitializeAttributeInstanceWithExternalValuesFromEntity(entityConfiguration, a, attributeValue); - } - - if (validationErrors.Count > 0) - { - return (null, new ValidationErrorResponse(validationErrors))!; - } - - if (!dryRun) - { - var entityConfigurationSaved = await _entityConfigurationRepository - .SaveAsync(_userInfo, entityConfiguration, cancellationToken) - .ConfigureAwait(false); - - if (!entityConfigurationSaved) - { - throw new Exception("Entity was not saved"); - } - - ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory - .FromEntityConfiguration(entityConfiguration, attributeConfigurations); - - IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); - await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - - var entityInstanceSaved = - await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, cancellationToken); - - if (!entityInstanceSaved) - { - //TODO: What do we want to do with internal exceptions and unsuccessful flow? - throw new Exception("Entity was not saved"); - } - - return (_mapper.Map(entityInstance), null); - } - - return (_mapper.Map(entityInstance), null); - } - - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "sku" and "name" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( - JsonElement entityJson, - CancellationToken cancellationToken = default - ) - { - Guid entityConfigurationId; - if (entityJson.TryGetProperty("entityConfigurationId", out var entityConfigurationIdJsonElement)) - { - if (entityConfigurationIdJsonElement.TryGetGuid(out var entityConfigurationIdGuid)) - { - entityConfigurationId = entityConfigurationIdGuid; - } - else - { - return (null, new ValidationErrorResponse("entityConfigurationId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("entityConfigurationId", "Value is missing")); - } - - Guid tenantId; - if (entityJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) - { - if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) - { - tenantId = tenantIdGuid; - } - else - { - return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("tenantId", "Value is missing")); - } - - return await DeserializeEntityInstanceCreateRequestFromJson( - entityJson, entityConfigurationId, tenantId, cancellationToken - ); - } - - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity" - /// } - /// ``` - /// - /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( - JsonElement entityJson, - Guid entityConfigurationId, - Guid tenantId, - CancellationToken cancellationToken = default - ) - { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entityConfigurationId, - entityConfigurationId.ToString(), - cancellationToken - ) - .ConfigureAwait(false); - - if (entityConfiguration == null) - { - return (null, new ValidationErrorResponse("EntityConfigurationId", "EntityConfiguration not found"))!; - } - - List attributeConfigurations = - await GetAttributeConfigurationsForEntityConfiguration( - entityConfiguration, - cancellationToken - ) - .ConfigureAwait(false); - - return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeEntityInstanceCreateRequest( - entityConfigurationId, tenantId, attributeConfigurations, entityJson - ); - } - - public async Task GetEntityInstance(Guid id, string partitionKey) - { - EntityInstance? entityInstance = await _entityInstanceRepository.LoadAsync(id, partitionKey); - - return _mapper.Map(entityInstance); - } - - public async Task GetEntityInstanceJsonMultiLanguage(Guid id, string partitionKey) - { - EntityInstanceViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); - - return SerializeEntityInstanceToJsonMultiLanguage(entityInstanceViewModel); - } - - public async Task GetEntityInstanceJsonSingleLanguage( - Guid id, - string partitionKey, - string language, - string fallbackLanguage = "en-US") - { - EntityInstanceViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); - - return SerializeEntityInstanceToJsonSingleLanguage(entityInstanceViewModel, language, fallbackLanguage); - } - - public JsonDocument SerializeEntityInstanceToJsonMultiLanguage(EntityInstanceViewModel? entityInstanceViewModel) - { - var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); - serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); - serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); - - return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); - } - - public JsonDocument SerializeEntityInstanceToJsonSingleLanguage( - EntityInstanceViewModel? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" - ) - { - var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); - serializerOptions.Converters.Add(new LocalizedStringSingleLanguageSerializer(language, fallbackLanguage)); - serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); - - return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); - } - - public async Task<(EntityInstanceViewModel, ProblemDetails)> UpdateEntityInstance( - string partitionKey, - EntityInstanceUpdateRequest updateRequest, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - EntityInstance? entityInstance = - await _entityInstanceRepository.LoadAsync(updateRequest.Id, partitionKey, cancellationToken); - - if (entityInstance == null) - { - return (null, new ValidationErrorResponse(nameof(updateRequest.Id), "Entity instance not found"))!; - } - - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entityInstance.EntityConfigurationId, - entityInstance.EntityConfigurationId.ToString(), - cancellationToken - ); - - if (entityConfiguration == null) - { - return (null, - new ValidationErrorResponse(nameof(updateRequest.EntityConfigurationId), - "Entity configuration not found" - ))!; - } - - List entityConfigurationAttributeConfigurations = - await GetAttributeConfigurationsForEntityConfiguration( - entityConfiguration, - cancellationToken - ); - - var validationErrors = new Dictionary(); - - if (updateRequest.AttributeMachineNamesToRemove != null) - { - foreach (var attributeMachineNameToRemove in updateRequest.AttributeMachineNamesToRemove) - { - AttributeConfiguration? attrConfiguration = entityConfigurationAttributeConfigurations - .First(c => c.MachineName == attributeMachineNameToRemove); - updateRequest.AttributesToAddOrUpdate.RemoveAll(a => - a.ConfigurationAttributeMachineName == attributeMachineNameToRemove); - - if (requiredAttributesCanBeNull) - { - entityInstance.RemoveAttributeInstance(attributeMachineNameToRemove); - continue; - } - - // validation against null will check if the attribute is required - List errors = attrConfiguration.ValidateInstance(null); - - if (errors.Count == 0) - { - entityInstance.RemoveAttributeInstance(attributeMachineNameToRemove); - } - else - { - validationErrors.Add(attributeMachineNameToRemove, errors.ToArray()); - } - } - } - - // Add or update attributes - foreach (AttributeInstanceCreateUpdateRequest? newAttributeRequest in updateRequest.AttributesToAddOrUpdate) + // Add or update attributes + foreach (AttributeInstanceCreateUpdateRequest? newAttributeRequest in updateRequest.AttributesToAddOrUpdate) { AttributeConfiguration? attrConfig = entityConfigurationAttributeConfigurations .FirstOrDefault(c => c.MachineName == newAttributeRequest.ConfigurationAttributeMachineName); @@ -2401,14 +1218,14 @@ await GetAttributeConfigurationsForEntityConfiguration( var entityInstanceSaved = await _entityInstanceRepository .SaveAsync(_userInfo, entityInstance, cancellationToken) - .ConfigureAwait(false); + .ConfigureAwait(false); if (!entityInstanceSaved || !entityConfigurationSaved) { //TODO: Throw a error when ready } } - return (_mapper.Map(entityInstance), null)!; + return (_mapper.Map(entityInstance), null)!; } /// @@ -2420,7 +1237,7 @@ await GetAttributeConfigurationsForEntityConfiguration( /// /// /// - public async Task> QueryInstances( + public async Task> QueryInstances( Guid entityConfigurationId, ProjectionQuery query, CancellationToken cancellationToken = default @@ -2545,17 +1362,17 @@ public async Task> QueryInstancesJsonSingleL ); } - public async Task<(EntityInstanceViewModel, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, + public async Task<(TViewModel, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, string entityInstancePartitionKey, Guid treeId, Guid? newParentId, CancellationToken cancellationToken = default) { - EntityInstance? entityInstance = await _entityInstanceRepository + TEntityType? entityInstance = await _entityInstanceRepository .LoadAsync(entityInstanceId, entityInstancePartitionKey, cancellationToken).ConfigureAwait(false); if (entityInstance == null) { return (null, new ValidationErrorResponse(nameof(entityInstanceId), "Instance not found"))!; } - (var newCategoryPath, ProblemDetails? errors) = + (var newCategoryPath, var parentId, ProblemDetails? errors) = await BuildCategoryPath(treeId, newParentId, cancellationToken).ConfigureAwait(false); if (errors != null) @@ -2563,7 +1380,7 @@ public async Task> QueryInstancesJsonSingleL return (null, errors)!; } - entityInstance.ChangeCategoryPath(treeId, newCategoryPath ?? ""); + entityInstance.ChangeCategoryPath(treeId, newCategoryPath ?? "", parentId!.Value); var saved = await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, cancellationToken) .ConfigureAwait(false); if (!saved) @@ -2572,10 +1389,10 @@ public async Task> QueryInstancesJsonSingleL throw new Exception("Entity was not saved"); } - return (_mapper.Map(entityInstance), null)!; + return (_mapper.Map(entityInstance), null)!; } - private async Task> GetAttributeConfigurationsForEntityConfiguration( + internal async Task> GetAttributeConfigurationsForEntityConfiguration( EntityConfiguration entityConfiguration, CancellationToken cancellationToken = default ) { @@ -2596,4 +1413,60 @@ await _attributeConfigurationRepository.LoadAsyncOrThrowNotFound( } #endregion + + internal void InitializeAttributeInstanceWithExternalValuesFromEntity( + EntityConfiguration entityConfiguration, + AttributeConfiguration attributeConfiguration, + AttributeInstance? attributeInstance + ) + { + switch (attributeConfiguration.ValueType) + { + case EavAttributeType.Serial: + { + if (attributeInstance == null) + { + return; + } + + var serialAttributeConfiguration = attributeConfiguration as SerialAttributeConfiguration; + + var serialInstance = attributeInstance as SerialAttributeInstance; + + if (serialAttributeConfiguration == null || serialInstance == null) + { + throw new ArgumentException("Invalid attribute type"); + } + + EntityConfigurationAttributeReference? entityAttribute = entityConfiguration.Attributes + .FirstOrDefault(x => x.AttributeConfigurationId == attributeConfiguration.Id); + + if (entityAttribute == null) + { + throw new NotFoundException("Attribute not found"); + } + + var existingAttributeValue = + entityAttribute.AttributeConfigurationExternalValues.FirstOrDefault(); + + long? deserializedValue = null; + + if (existingAttributeValue != null) + { + deserializedValue = JsonSerializer.Deserialize(existingAttributeValue.ToString()!); + } + + var newExternalValue = existingAttributeValue == null + ? serialAttributeConfiguration.StartingNumber + : deserializedValue += serialAttributeConfiguration.Increment; + + serialInstance.Value = newExternalValue!.Value; + + entityConfiguration.UpdateAttrributeExternalValues(attributeConfiguration.Id, + new List { newExternalValue } + ); + } + break; + } + } } diff --git a/CloudFabric.EAV.Service/IEAVService.cs b/CloudFabric.EAV.Service/IEAVService.cs deleted file mode 100644 index b82fa7f..0000000 --- a/CloudFabric.EAV.Service/IEAVService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace CloudFabric.EAV.Service; - -public interface IEAVService -{ -} diff --git a/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs b/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs index 6e97c7b..3ac9ed1 100644 --- a/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs +++ b/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs @@ -15,9 +15,7 @@ public EntityInstanceProfile() CreateMap(); CreateMap(); - CreateMap().ForMember(o => o.Children, - opt => opt.MapFrom(_ => new List()) - ); + CreateMap(); CreateMap(); @@ -25,6 +23,9 @@ public EntityInstanceProfile() CreateMap(); CreateMap(); CreateMap(); + CreateMap().ForMember(o => o.Children, + opt => opt.MapFrom(_ => new List()) + ); CreateMap(); CreateMap(); diff --git a/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs b/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs index dc8204b..377cc21 100644 --- a/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs +++ b/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs @@ -53,6 +53,7 @@ JsonElement record public async Task<(CategoryInstanceCreateRequest?, ValidationErrorResponse?)> DeserializeCategoryInstanceCreateRequest( Guid categoryConfigurationId, + string machineName, Guid? tenantId, Guid categoryTreeId, Guid? parentId, @@ -74,7 +75,8 @@ JsonElement record CategoryTreeId = categoryTreeId, ParentId = parentId, TenantId = tenantId, - Attributes = attributes + Attributes = attributes, + MachineName = machineName }; return (categoryInstanceCreateRequest, null); diff --git a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs index b99c277..1876bea 100644 --- a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs +++ b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs @@ -8,64 +8,16 @@ namespace CloudFabric.EAV.Service.Serialization; -/// -/// Entities are stored as c# dictionaries in projections - something similar to json. -/// That is required to not overload search engines with additional complexity of entity instances and attributes -/// allowing us to simply store -/// photo.likes = 4 instead of photo.attributes.where(a => a.machineName == "likes").value = 4 -/// -/// That comes with a price though - we now have to decode json-like dictionary back to entity instance view model. -/// Also it becomes not clear where is a serialization part and where is a deserializer. -/// -/// The following structure seems logical, not very understandable from the first sight however: -/// -/// -/// Serialization happens in -/// Projection builder creates dictionaries from EntityInstances and is responsible for storing projections data in -/// the best way suitable for search engines like elasticsearch. -/// -/// The segregation of reads and writes moves our decoding code out of ProjectionBuilder -/// and even out of CloudFabric.EAV.Domain because our ViewModels are on another layer - same layer as a service. -/// That means it's a service concern to decode dictionary into a ViewModel. -/// -/// -/// -public class EntityInstanceFromDictionaryDeserializer +public abstract class InstanceFromDictionaryDeserializer where T: EntityInstanceViewModel { - private readonly IMapper _mapper; - - public EntityInstanceFromDictionaryDeserializer(IMapper mapper) - { - _mapper = mapper; - } + internal IMapper _mapper { get; set; } - public EntityInstanceViewModel Deserialize( + public abstract T Deserialize( List attributesConfigurations, Dictionary record - ) - { - var entityInstance = new EntityInstanceViewModel - { - Id = (Guid)record["Id"]!, - TenantId = record.ContainsKey("TenantId") && record["TenantId"] != null - ? (Guid)record["TenantId"]! - : null, - EntityConfigurationId = (Guid)record["EntityConfigurationId"]!, - PartitionKey = (string)record["PartitionKey"]!, - Attributes = attributesConfigurations - .Where(attributeConfig => record.ContainsKey(attributeConfig.MachineName)) - .Select(attributeConfig => - DeserializeAttribute(attributeConfig, record[attributeConfig.MachineName]) - ) - .ToList(), - CategoryPaths = record.ContainsKey("CategoryPaths") - ? ParseCategoryPaths(record["CategoryPaths"]) - : new List() - }; - return entityInstance; - } + ); - private List ParseCategoryPaths(object? paths) + internal List ParseCategoryPaths(object? paths) { var categoryPaths = new List(); if (paths is List pathsList) @@ -85,6 +37,14 @@ private List ParseCategoryPaths(object? paths) { categoryPath.TreeId = (Guid)pathItem.Value; } + else if (pathItem.Key == "parentId") + { + categoryPath.ParentId = (Guid)pathItem.Value; + } + else if (pathItem.Key == "parentMachineName") + { + categoryPath.ParentMachineName = (string)pathItem.Value; + } } categoryPaths.Add(categoryPath); @@ -103,7 +63,7 @@ private List ParseCategoryPaths(object? paths) return categoryPaths; } - private AttributeInstanceViewModel DeserializeAttribute( + internal AttributeInstanceViewModel DeserializeAttribute( AttributeConfiguration attributeConfiguration, object? attributeValue ) @@ -145,7 +105,7 @@ private AttributeInstanceViewModel DeserializeAttribute( return attributeInstance; } - private AttributeInstanceViewModel DeserializeAttribute( + internal AttributeInstanceViewModel DeserializeAttribute( string attributeMachineName, EavAttributeType attributeType, object? attributeValue @@ -214,3 +174,97 @@ private AttributeInstanceViewModel DeserializeAttribute( return attributeInstance; } } + +/// +/// Entities are stored as c# dictionaries in projections - something similar to json. +/// That is required to not overload search engines with additional complexity of entity instances and attributes +/// allowing us to simply store +/// photo.likes = 4 instead of photo.attributes.where(a => a.machineName == "likes").value = 4 +/// +/// That comes with a price though - we now have to decode json-like dictionary back to entity instance view model. +/// Also it becomes not clear where is a serialization part and where is a deserializer. +/// +/// The following structure seems logical, not very understandable from the first sight however: +/// +/// +/// Serialization happens in +/// Projection builder creates dictionaries from EntityInstances and is responsible for storing projections data in +/// the best way suitable for search engines like elasticsearch. +/// +/// The segregation of reads and writes moves our decoding code out of ProjectionBuilder +/// and even out of CloudFabric.EAV.Domain because our ViewModels are on another layer - same layer as a service. +/// That means it's a service concern to decode dictionary into a ViewModel. +/// +/// +/// +public class EntityInstanceFromDictionaryDeserializer: InstanceFromDictionaryDeserializer +{ + + public EntityInstanceFromDictionaryDeserializer(IMapper mapper) + { + _mapper = mapper; + } + + public override EntityInstanceViewModel Deserialize( + List attributesConfigurations, + Dictionary record + ) + { + var entityInstance = new EntityInstanceViewModel + { + Id = (Guid)record["Id"]!, + TenantId = record.ContainsKey("TenantId") && record["TenantId"] != null + ? (Guid)record["TenantId"]! + : null, + EntityConfigurationId = (Guid)record["EntityConfigurationId"]!, + PartitionKey = (string)record["PartitionKey"]!, + Attributes = attributesConfigurations + .Where(attributeConfig => record.ContainsKey(attributeConfig.MachineName)) + .Select(attributeConfig => + DeserializeAttribute(attributeConfig, record[attributeConfig.MachineName]) + ) + .ToList(), + CategoryPaths = record.ContainsKey("CategoryPaths") + ? ParseCategoryPaths(record["CategoryPaths"]) + : new List() + }; + return entityInstance; + } + +} + +public class CategoryFromDictionaryDeserializer : InstanceFromDictionaryDeserializer +{ + + public CategoryFromDictionaryDeserializer(IMapper mapper) + { + _mapper = mapper; + } + + public override CategoryViewModel Deserialize( + List attributesConfigurations, + Dictionary record + ) + { + var category = new CategoryViewModel() + { + Id = (Guid)record["Id"]!, + TenantId = record.ContainsKey("TenantId") && record["TenantId"] != null + ? (Guid)record["TenantId"]! + : null, + EntityConfigurationId = (Guid)record["EntityConfigurationId"]!, + PartitionKey = (string)record["PartitionKey"]!, + Attributes = attributesConfigurations + .Where(attributeConfig => record.ContainsKey(attributeConfig.MachineName)) + .Select(attributeConfig => + DeserializeAttribute(attributeConfig, record[attributeConfig.MachineName]) + ) + .ToList(), + CategoryPaths = record.ContainsKey("CategoryPaths") + ? ParseCategoryPaths(record["CategoryPaths"]) + : new List(), + MachineName = (string)record["MachineName"]! + }; + return category; + } +} diff --git a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs index 3ffcbda..8f7b297 100644 --- a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs +++ b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs @@ -3,74 +3,158 @@ using AutoMapper; +using CloudFabric.EAV.Domain.Models; using CloudFabric.EAV.Domain.Projections.AttributeConfigurationProjection; +using CloudFabric.EAV.Domain.Projections.EntityConfigurationProjection; using CloudFabric.EAV.Domain.Projections.EntityInstanceProjection; using CloudFabric.EAV.Service; using CloudFabric.EventSourcing.Domain; using CloudFabric.EventSourcing.EventStore; using CloudFabric.EventSourcing.EventStore.Persistence; using CloudFabric.Projections; +using CloudFabric.Projections.Worker; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.BaseQueryTests; public abstract class BaseQueryTests { - protected EAVService _eavService; + protected EAVEntityInstanceService _eavEntityInstanceService; + protected EAVCategoryService _eavCategoryService; + protected IEventStore _eventStore; - protected ILogger _logger; + protected IStore _store; + protected ILogger _eiLogger; + protected ILogger _cLogger; protected virtual TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(0); protected abstract IEventStore GetEventStore(); + protected abstract IStore GetStore(); protected abstract ProjectionRepositoryFactory GetProjectionRepositoryFactory(); - protected abstract IEventsObserver GetEventStoreEventsObserver(); + protected abstract EventsObserver GetEventStoreEventsObserver(); + + protected ProjectionsRebuildProcessor ProjectionsRebuildProcessor; [TestInitialize] public async Task SetUp() { var loggerFactory = new LoggerFactory(); - _logger = loggerFactory.CreateLogger(); + _eiLogger = loggerFactory.CreateLogger(); + _cLogger = loggerFactory.CreateLogger(); - var configuration = new MapperConfiguration(cfg => + var eiConfiguration = new MapperConfiguration(cfg => { - cfg.AddMaps(Assembly.GetAssembly(typeof(EAVService))); + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVEntityInstanceService))); } ); - IMapper? mapper = configuration.CreateMapper(); + var cConfiguration = new MapperConfiguration(cfg => + { + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVCategoryService))); + } + ); + IMapper? eiMapper = eiConfiguration.CreateMapper(); + IMapper? cMapper = eiConfiguration.CreateMapper(); _eventStore = GetEventStore(); await _eventStore.Initialize(); + await _eventStore.DeleteAll(); + + _store = GetStore(); + await _store.Initialize(); var aggregateRepositoryFactory = new AggregateRepositoryFactory(_eventStore); ProjectionRepositoryFactory projectionRepositoryFactory = GetProjectionRepositoryFactory(); + var projectionRepository = projectionRepositoryFactory + .GetProjectionRepository( + new ProjectionDocumentSchema + { + SchemaName = "" + } + ); + await projectionRepository.DeleteAll(); // Projections engine - takes events from events observer and passes them to multiple projection builders - var projectionsEngine = new ProjectionsEngine( - projectionRepositoryFactory.GetProjectionRepository() - ); + var projectionsEngine = new ProjectionsEngine(); projectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder = new AttributeConfigurationProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write + ); + + var entityConfigurationProjectionBuilder = new EntityConfigurationProjectionBuilder( + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); var entityInstanceProjectionBuilder = new EntityInstanceProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, aggregateRepositoryFactory, ProjectionOperationIndexSelector.Write ); projectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder); + projectionsEngine.AddProjectionBuilder(entityConfigurationProjectionBuilder); projectionsEngine.AddProjectionBuilder(entityInstanceProjectionBuilder); await projectionsEngine.StartAsync("TestInstance"); - _eavService = new EAVService( - _logger, - mapper, - new JsonSerializerOptions() + ProjectionsRebuildProcessor = new ProjectionsRebuildProcessor( + GetProjectionRepositoryFactory().GetProjectionsIndexStateRepository(), + async (string connectionId) => + { + var rebuildProjectionsEngine = new ProjectionsEngine(); + rebuildProjectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); + + var attributeConfigurationProjectionBuilder2 = new AttributeConfigurationProjectionBuilder( + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write + ); + + var entityConfigurationProjectionBuilder2 = new EntityConfigurationProjectionBuilder( + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write + ); + + var entityInstanceProjectionBuilder2 = new EntityInstanceProjectionBuilder( + projectionRepositoryFactory, aggregateRepositoryFactory, ProjectionOperationIndexSelector.Write + ); + + rebuildProjectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder2); + rebuildProjectionsEngine.AddProjectionBuilder(entityConfigurationProjectionBuilder2); + rebuildProjectionsEngine.AddProjectionBuilder(entityInstanceProjectionBuilder2); + + return rebuildProjectionsEngine; + }, + NullLogger.Instance + ); + + var attributeConfigurationProjectionRepository = + projectionRepositoryFactory.GetProjectionRepository(); + await attributeConfigurationProjectionRepository.EnsureIndex(); + + var entityConfigurationProjectionRepository = + projectionRepositoryFactory.GetProjectionRepository(); + await entityConfigurationProjectionRepository.EnsureIndex(); + + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + + _eavEntityInstanceService = new EAVEntityInstanceService( + _eiLogger, + eiMapper, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + }, + aggregateRepositoryFactory, + projectionRepositoryFactory, + new EventUserInfo(Guid.NewGuid()) + ); + + _eavCategoryService = new EAVCategoryService( + _cLogger, + cMapper, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DictionaryKeyPolicy = JsonNamingPolicy.CamelCase diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs index 32a4fe5..e056dff 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs @@ -1,4 +1,4 @@ -using CloudFabric.EAV.Models.RequestModels; + using CloudFabric.EAV.Models.RequestModels; using CloudFabric.EAV.Models.ViewModels; using CloudFabric.EAV.Tests.Factories; using CloudFabric.EventSourcing.EventStore; @@ -16,6 +16,12 @@ namespace CloudFabric.EAV.Tests.CategoryTests; public abstract class CategoryTests : BaseQueryTests.BaseQueryTests { + private const string _laptopsCategoryMachineName = "laptops"; + private const string _gamingLaptopsCategoryMachineName = "gaming-laptops"; + private const string _officeLaptopsCategoryMachineName = "office-laptops"; + private const string _asusGamingLaptopsCategoryMachineName = "asus-gaming-laptops"; + private const string _rogAsusGamingLaptopsCategoryMachineName = "rog-gaming-laptops"; + private async Task<(HierarchyViewModel tree, CategoryViewModel laptopsCategory, CategoryViewModel gamingLaptopsCategory, @@ -26,11 +32,13 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests // Create config for categories EntityConfigurationCreateRequest categoryConfigurationCreateRequest = EntityConfigurationFactory.CreateBoardGameCategoryConfigurationCreateRequest(0, 9); - (EntityConfigurationViewModel? categoryConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? categoryConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( categoryConfigurationCreateRequest, CancellationToken.None ); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + // Create a tree var treeRequest = new CategoryTreeCreateRequest { @@ -38,40 +46,60 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests EntityConfigurationId = categoryConfiguration!.Id }; - (HierarchyViewModel createdTree, _) = await _eavService.CreateCategoryTreeAsync(treeRequest, + (HierarchyViewModel createdTree, _) = await _eavCategoryService.CreateCategoryTreeAsync(treeRequest, categoryConfigurationCreateRequest.TenantId, CancellationToken.None ); - CategoryInstanceCreateRequest categoryInstanceRequest = - EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + (CategoryViewModel laptopsCategory, _) = + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, null, categoryConfigurationCreateRequest.TenantId, + _laptopsCategoryMachineName, 0, 9 - ); - + )); - (CategoryViewModel laptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); - - categoryInstanceRequest.ParentId = laptopsCategory.Id; (CategoryViewModel gamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + laptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _gamingLaptopsCategoryMachineName, + 0, + 9 + )); - categoryInstanceRequest.ParentId = laptopsCategory.Id; (CategoryViewModel officeLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + laptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _officeLaptopsCategoryMachineName, + 0, + 9 + )); - categoryInstanceRequest.ParentId = gamingLaptopsCategory.Id; (CategoryViewModel asusGamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + gamingLaptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _asusGamingLaptopsCategoryMachineName, + 0, + 9 + )); - categoryInstanceRequest.ParentId = asusGamingLaptopsCategory.Id; (CategoryViewModel rogAsusGamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); - + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + asusGamingLaptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _rogAsusGamingLaptopsCategoryMachineName, + 0, + 9 + )); await Task.Delay(ProjectionsUpdateDelay); return (createdTree, laptopsCategory, gamingLaptopsCategory, officeLaptopsCategory, @@ -87,7 +115,7 @@ public async Task CreateCategory_Success() laptopsCategory.Id.Should().NotBeEmpty(); laptopsCategory.Attributes.Count.Should().Be(9); laptopsCategory.TenantId.Should().NotBeNull(); - gamingLaptopsCategory.CategoryPaths.Should().Contain(x => x.Path.Contains(laptopsCategory.Id.ToString())); + gamingLaptopsCategory.CategoryPaths.Should().Contain(x => x.Path.Contains(laptopsCategory.MachineName)); } [TestMethod] @@ -98,7 +126,7 @@ public async Task GetTreeViewAsync() CategoryViewModel rogAsusGamingLaptopsCategory) = await BuildTestTreeAsync(); List list = - await _eavService.GetCategoryTreeViewAsync(createdTree.Id); + await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id); EntityTreeInstanceViewModel? laptops = list.FirstOrDefault(x => x.Id == laptopsCategory.Id); laptops.Should().NotBeNull(); @@ -116,11 +144,11 @@ public async Task GetTreeViewAsync() asusGamingLaptops?.Children.FirstOrDefault(x => x.Id == rogAsusGamingLaptopsCategory.Id); rogAsusGamingLaptops.Should().NotBeNull(); - list = await _eavService.GetCategoryTreeViewAsync(createdTree.Id, laptopsCategory.Id); + list = await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id, laptopsCategory.Id); laptops = list.FirstOrDefault(x => x.Id == laptopsCategory.Id); laptops.Children.Count.Should().Be(0); - list = await _eavService.GetCategoryTreeViewAsync(createdTree.Id, asusGamingLaptopsCategory.Id); + list = await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id, asusGamingLaptopsCategory.Id); laptops = list.FirstOrDefault(x => x.Id == laptopsCategory.Id); gamingLaptops = laptops.Children.FirstOrDefault(x => x.Id == gamingLaptopsCategory.Id); @@ -139,7 +167,7 @@ public async Task GetTreeViewAsync_CategoryNofFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - Func action = async () => await _eavService.GetCategoryTreeViewAsync(createdTree.Id, Guid.NewGuid()); + Func action = async () => await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id, Guid.NewGuid()); await action.Should().ThrowAsync(); } @@ -151,8 +179,8 @@ public async Task GetSubcategoriesBranch_Success() _, _, _) = await BuildTestTreeAsync(); await Task.Delay(ProjectionsUpdateDelay); - var categoryPathValue = $"/{laptopsCategory.Id}/{gamingLaptopsCategory.Id}"; - ProjectionQueryResult subcategories12 = await _eavService.QueryInstances( + var categoryPathValue = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}"; + ProjectionQueryResult subcategories12 = await _eavEntityInstanceService.QueryInstances( createdTree.EntityConfigurationId, new ProjectionQuery { @@ -174,19 +202,19 @@ public async Task GetSubcategories_Success() CategoryViewModel gamingLaptopsCategory, CategoryViewModel officeLaptopsCategory, CategoryViewModel asusGamingLaptops, CategoryViewModel _) = await BuildTestTreeAsync(); - var subcategories = await _eavService.GetSubcategories(createdTree.Id, null); + var subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, null); subcategories.Count.Should().Be(1); - subcategories = await _eavService.GetSubcategories(createdTree.Id, laptopsCategory.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, laptopsCategory.Id); subcategories.Count.Should().Be(2); - subcategories = await _eavService.GetSubcategories(createdTree.Id, gamingLaptopsCategory.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, gamingLaptopsCategory.Id); subcategories.Count.Should().Be(1); - subcategories = await _eavService.GetSubcategories(createdTree.Id, asusGamingLaptops.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, asusGamingLaptops.Id); subcategories.Count.Should().Be(1); - subcategories = await _eavService.GetSubcategories(createdTree.Id, officeLaptopsCategory.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, officeLaptopsCategory.Id); subcategories.Count.Should().Be(0); } @@ -197,7 +225,7 @@ public async Task GetSubcategories_TreeNotFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - Func action = async () => await _eavService.GetSubcategories(Guid.NewGuid(), null); + Func action = async () => await _eavCategoryService.GetSubcategories(Guid.NewGuid()); await action.Should().ThrowAsync().WithMessage("Category tree not found"); } @@ -209,9 +237,8 @@ public async Task GetSubcategories_ParentNotFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - Func action = async () => await _eavService.GetSubcategories(createdTree.Id, Guid.NewGuid()); - - await action.Should().ThrowAsync().WithMessage("Category not found"); + var result = await _eavCategoryService.GetSubcategories(createdTree.Id, parentId: Guid.NewGuid()); + result.Should().BeEmpty(); } [TestMethod] @@ -223,19 +250,21 @@ public async Task MoveAndGetItemsFromCategory_Success() EntityConfigurationCreateRequest itemEntityConfig = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? itemEntityConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? itemEntityConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( itemEntityConfig, CancellationToken.None ); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + EntityInstanceCreateRequest itemInstanceRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(itemEntityConfiguration.Id); - var (_, _) = await _eavService.CreateEntityInstance(itemInstanceRequest); + var (_, _) = await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); (EntityInstanceViewModel createdItemInstance2, _) = - await _eavService.CreateEntityInstance(itemInstanceRequest); - (createdItemInstance2, _) = await _eavService.UpdateCategoryPath(createdItemInstance2.Id, + await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); + (createdItemInstance2, _) = await _eavEntityInstanceService.UpdateCategoryPath(createdItemInstance2.Id, createdItemInstance2.PartitionKey, createdTree.Id, asusGamingLaptops.Id, @@ -243,8 +272,8 @@ public async Task MoveAndGetItemsFromCategory_Success() ); (EntityInstanceViewModel createdItemInstance3, _) = - await _eavService.CreateEntityInstance(itemInstanceRequest); - (_, _) = await _eavService.UpdateCategoryPath(createdItemInstance3.Id, + await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); + (_, _) = await _eavEntityInstanceService.UpdateCategoryPath(createdItemInstance3.Id, createdItemInstance2.PartitionKey, createdTree.Id, rogAsusGamingLaptops.Id, @@ -253,10 +282,10 @@ public async Task MoveAndGetItemsFromCategory_Success() await Task.Delay(ProjectionsUpdateDelay); - var pathFilterValue121 = $"/{laptopsCategory.Id}/{gamingLaptopsCategory.Id}/{asusGamingLaptops.Id}"; + var pathFilterValue121 = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}"; - ProjectionQueryResult itemsFrom121 = await _eavService.QueryInstances( + ProjectionQueryResult itemsFrom121 = await _eavEntityInstanceService.QueryInstances( itemEntityConfiguration.Id, new ProjectionQuery { @@ -268,9 +297,9 @@ public async Task MoveAndGetItemsFromCategory_Success() } ); var pathFilterValue1211 = - $"/{laptopsCategory.Id}/{gamingLaptopsCategory.Id}/{asusGamingLaptops.Id}/{rogAsusGamingLaptops.Id}"; + $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}/{_rogAsusGamingLaptopsCategoryMachineName}"; - ProjectionQueryResult itemsFrom1211 = await _eavService.QueryInstances( + ProjectionQueryResult itemsFrom1211 = await _eavEntityInstanceService.QueryInstances( itemEntityConfiguration.Id, new ProjectionQuery { diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs index d659254..17283c1 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs @@ -3,6 +3,7 @@ using CloudFabric.Projections; using CloudFabric.Projections.Postgresql; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.CategoryTests; @@ -11,6 +12,7 @@ namespace CloudFabric.EAV.Tests.CategoryTests; public class CategoryTestsPostgresql : CategoryTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public CategoryTestsPostgresql() { @@ -22,9 +24,15 @@ public CategoryTestsPostgresql() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_items_store" ); - _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(connectionString); + _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(new LoggerFactory(), connectionString); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + using var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); } protected override IEventStore GetEventStore() @@ -32,9 +40,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs index d630070..fd92f97 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs @@ -12,6 +12,7 @@ namespace CloudFabric.EAV.Tests.CategoryTests; public class CategoryTestsPostgresqlWithElasticSearch : CategoryTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public CategoryTestsPostgresqlWithElasticSearch() { @@ -23,7 +24,8 @@ public CategoryTestsPostgresqlWithElasticSearch() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); _projectionRepositoryFactory = new ElasticSearchProjectionRepositoryFactory( @@ -32,8 +34,14 @@ public CategoryTestsPostgresqlWithElasticSearch() "", "", ""), - new LoggerFactory() + new LoggerFactory(), + true ); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); } protected override TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(1000); @@ -43,9 +51,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj b/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj index 7778ebd..8ff2bc3 100644 --- a/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj +++ b/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj @@ -11,11 +11,12 @@ - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs index 9055773..6772831 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs @@ -28,12 +28,14 @@ public async Task TestCreateInstanceAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -43,14 +45,13 @@ public async Task TestCreateInstanceAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance(instanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); createdInstance.Attributes.Should() .BeEquivalentTo(instanceCreateRequest.Attributes, x => x.Excluding(w => w.ValueType)); - var query = new ProjectionQuery { Filters = new List { new("Id", FilterOperator.Equal, createdInstance.Id) } @@ -58,7 +59,7 @@ public async Task TestCreateInstanceAndQuery() await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -72,12 +73,12 @@ public async Task TestCreateInstanceUpdateAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -87,7 +88,7 @@ public async Task TestCreateInstanceUpdateAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance(instanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); @@ -102,7 +103,7 @@ public async Task TestCreateInstanceUpdateAndQuery() await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -119,7 +120,7 @@ public async Task TestCreateInstanceUpdateAndQuery() }; (EntityInstanceViewModel updateResult, ProblemDetails updateErrors) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), new EntityInstanceUpdateRequest { Id = createdInstance.Id, @@ -132,7 +133,7 @@ await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? searchResultsAfterUpdate = await _eavService + ProjectionQueryResult? searchResultsAfterUpdate = await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); searchResultsAfterUpdate?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -149,7 +150,7 @@ await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), .String.Should() .Be("Азул 2"); - var resultsJson = await _eavService + var resultsJson = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); var resultString = JsonSerializer.Serialize(resultsJson); diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs index 7778a89..5c2fa4b 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs @@ -3,6 +3,7 @@ using CloudFabric.Projections; using CloudFabric.Projections.InMemory; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; @@ -11,16 +12,21 @@ namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; public class EntityInstanceQueryingTestsInMemory : EntityInstanceQueryingTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public EntityInstanceQueryingTestsInMemory() { + var loggerFactory = new LoggerFactory(); + _eventStore = new InMemoryEventStore(new Dictionary<(Guid, string), List>()); - _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(); + _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(loggerFactory); + _store = new InMemoryStore(new Dictionary<(string, string), string>()); + _logger = loggerFactory.CreateLogger(); } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override EventsObserver GetEventStoreEventsObserver() { - return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore); + return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore, _logger); } protected override IEventStore GetEventStore() @@ -28,6 +34,11 @@ protected override IEventStore GetEventStore() return _eventStore; } + protected override IStore GetStore() + { + return _store; + } + protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() { return _projectionRepositoryFactory; diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs index f07e4de..4bdeb52 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs @@ -3,6 +3,7 @@ using CloudFabric.Projections; using CloudFabric.Projections.Postgresql; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; @@ -11,6 +12,7 @@ namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; public class EntityInstanceQueryingTestsPostgresql : EntityInstanceQueryingTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public EntityInstanceQueryingTestsPostgresql() { @@ -22,9 +24,17 @@ public EntityInstanceQueryingTestsPostgresql() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); - _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(connectionString); + + var loggerFactory = new LoggerFactory(); + + _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(loggerFactory, connectionString); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + _logger = loggerFactory.CreateLogger(); } protected override IEventStore GetEventStore() @@ -32,9 +42,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs index be3ed0c..d072db6 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs @@ -12,6 +12,7 @@ namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; public class EntityInstanceQueryingTestsPostgresqlWithElasticSearch : EntityInstanceQueryingTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public EntityInstanceQueryingTestsPostgresqlWithElasticSearch() { @@ -23,17 +24,24 @@ public EntityInstanceQueryingTestsPostgresqlWithElasticSearch() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); + var loggerFactory = new LoggerFactory(); + _projectionRepositoryFactory = new ElasticSearchProjectionRepositoryFactory( new ElasticSearchBasicAuthConnectionSettings( "http://127.0.0.1:9200", "", "", ""), - new LoggerFactory() + loggerFactory ); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + _logger = loggerFactory.CreateLogger(); } protected override TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(1000); @@ -43,9 +51,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs b/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs index 3a6bccf..d93beee 100644 --- a/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs +++ b/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs @@ -11,6 +11,7 @@ public static CategoryInstanceCreateRequest CreateCategoryInstanceRequest(Guid e Guid treeId, Guid? parentId, Guid? tenantId, + string machineName, int attributeIndexFrom = 0, int attributeIndexTo = 1) { @@ -30,7 +31,8 @@ public static CategoryInstanceCreateRequest CreateCategoryInstanceRequest(Guid e Attributes = attributeInstances, ParentId = parentId, TenantId = tenantId, - CategoryTreeId = treeId + CategoryTreeId = treeId, + MachineName = machineName }; } diff --git a/CloudFabric.EAV.Tests/JsonSerializationTests.cs b/CloudFabric.EAV.Tests/JsonSerializationTests.cs index fa43e18..22c5a58 100644 --- a/CloudFabric.EAV.Tests/JsonSerializationTests.cs +++ b/CloudFabric.EAV.Tests/JsonSerializationTests.cs @@ -13,6 +13,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests; @@ -21,16 +22,24 @@ namespace CloudFabric.EAV.Tests; public class JsonSerializationTests : BaseQueryTests.BaseQueryTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public JsonSerializationTests() { - _eventStore = new InMemoryEventStore(new Dictionary<(Guid, string), List>()); - _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(); + _eventStore = new InMemoryEventStore( + new Dictionary<(Guid, string), List>() + ); + _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(new LoggerFactory()); + + _store = new InMemoryStore(new Dictionary<(string, string), string>()); + + using var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override EventsObserver GetEventStoreEventsObserver() { - return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore); + return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore, _logger); } protected override IEventStore GetEventStore() @@ -38,6 +47,11 @@ protected override IEventStore GetEventStore() return _eventStore; } + protected override IStore GetStore() + { + return _store; + } + protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() { return _projectionRepositoryFactory; @@ -49,12 +63,14 @@ public async Task TestCreateInstanceMultiLangAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration!.Id ); @@ -64,7 +80,7 @@ public async Task TestCreateInstanceMultiLangAndQuery() .CreateValidBoardGameEntityInstanceCreateRequestJsonMultiLanguage(createdConfiguration.Id); (JsonDocument createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance( + await _eavEntityInstanceService.CreateEntityInstance( instanceCreateRequest, configuration.Id, configuration.TenantId.Value @@ -86,7 +102,7 @@ await _eavService.CreateEntityInstance( await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -123,7 +139,7 @@ await _eavService.CreateEntityInstance( .Should() .Be(createdInstance.RootElement.GetProperty("release_date").GetProperty("from").GetDateTime()); - ProjectionQueryResult resultsJsonMultiLanguage = await _eavService + ProjectionQueryResult resultsJsonMultiLanguage = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") @@ -131,7 +147,7 @@ await _eavService.CreateEntityInstance( resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") .GetProperty("ru-RU").GetString().Should().Be("Азул"); - ProjectionQueryResult resultsJsonSingleLanguage = await _eavService + ProjectionQueryResult resultsJsonSingleLanguage = await _eavEntityInstanceService .QueryInstancesJsonSingleLanguage( createdConfiguration.Id, query, "en-US" ); @@ -139,7 +155,7 @@ await _eavService.CreateEntityInstance( resultsJsonSingleLanguage.Records.First().Document.RootElement.GetProperty("name") .GetString().Should().Be("Azul"); - ProjectionQueryResult? resultsJsonSingleLanguageRu = await _eavService + ProjectionQueryResult? resultsJsonSingleLanguageRu = await _eavEntityInstanceService .QueryInstancesJsonSingleLanguage( createdConfiguration.Id, query, "ru-RU" ); @@ -153,14 +169,14 @@ await _eavService.CreateEntityInstance( var firstDocumentId = resultsJsonMultiLanguage.Records[0]?.Document!.RootElement.GetProperty("id").GetString(); - var oneInstanceJsonMultiLang = await _eavService.GetEntityInstanceJsonMultiLanguage( + var oneInstanceJsonMultiLang = await _eavEntityInstanceService.GetEntityInstanceJsonMultiLanguage( Guid.Parse(firstDocumentId!), createdInstance.RootElement.GetProperty("entityConfigurationId").GetString()! ); oneInstanceJsonMultiLang.RootElement.GetProperty("name").GetProperty("en-US").GetString().Should().Be("Azul"); - var oneInstanceJsonSingleLang = await _eavService.GetEntityInstanceJsonSingleLanguage( + var oneInstanceJsonSingleLang = await _eavEntityInstanceService.GetEntityInstanceJsonSingleLanguage( Guid.Parse(firstDocumentId!), createdInstance.RootElement.GetProperty("entityConfigurationId").GetString()!, "en-US" @@ -175,12 +191,12 @@ public async Task TestCreateInstanceSingleLangAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration!.Id ); @@ -190,7 +206,7 @@ public async Task TestCreateInstanceSingleLangAndQuery() .CreateValidBoardGameEntityInstanceCreateRequestJsonSingleLanguage(createdConfiguration.Id); (JsonDocument createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance( + await _eavEntityInstanceService.CreateEntityInstance( instanceCreateRequest, configuration.Id, configuration.TenantId.Value @@ -212,7 +228,7 @@ await _eavService.CreateEntityInstance( await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -249,7 +265,7 @@ await _eavService.CreateEntityInstance( .Should() .Be(createdInstance.RootElement.GetProperty("release_date").GetProperty("from").GetDateTime()); - ProjectionQueryResult resultsJsonMultiLanguage = await _eavService + ProjectionQueryResult resultsJsonMultiLanguage = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") @@ -266,9 +282,9 @@ public async Task CreateCategoryInstance() EntityConfigurationFactory.CreateBoardGameCategoryConfigurationCreateRequest(); (EntityConfigurationViewModel? createdCategoryConfiguration, _) = - await _eavService.CreateEntityConfiguration(categoryConfigurationRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(categoryConfigurationRequest, CancellationToken.None); - (HierarchyViewModel hierarchy, _) = await _eavService.CreateCategoryTreeAsync( + (HierarchyViewModel hierarchy, _) = await _eavCategoryService.CreateCategoryTreeAsync( new CategoryTreeCreateRequest { EntityConfigurationId = createdCategoryConfiguration!.Id, @@ -277,7 +293,9 @@ public async Task CreateCategoryInstance() }, categoryConfigurationRequest.TenantId); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdCategoryConfiguration!.Id ); @@ -288,7 +306,7 @@ public async Task CreateCategoryInstance() createdCategoryConfiguration!.Id, hierarchy.Id, createdCategoryConfiguration.TenantId!.Value ); - (JsonDocument? createdCategory, _) = await _eavService.CreateCategoryInstance(categoryJsonStringCreateRequest); + (JsonDocument? createdCategory, _) = await _eavCategoryService.CreateCategoryInstance(categoryJsonStringCreateRequest); var query = new ProjectionQuery { @@ -298,7 +316,7 @@ public async Task CreateCategoryInstance() } }; - var results = await _eavService.QueryInstancesJsonSingleLanguage(createdCategoryConfiguration.Id, query); + var results = await _eavEntityInstanceService.QueryInstancesJsonSingleLanguage(createdCategoryConfiguration.Id, query); var resultDocument = results?.Records.Select(r => r.Document).First(); @@ -308,8 +326,9 @@ public async Task CreateCategoryInstance() JsonSerializer.Deserialize>(resultDocument!.RootElement.GetProperty("categoryPaths"), _serializerOptions)! .First().TreeId.Should().Be(hierarchy.Id); - (createdCategory, _) = await _eavService.CreateCategoryInstance( + (createdCategory, _) = await _eavCategoryService.CreateCategoryInstance( categoryJsonStringCreateRequest, + "test-category", createdCategoryConfiguration.Id, hierarchy.Id, null, diff --git a/CloudFabric.EAV.Tests/Tests.cs b/CloudFabric.EAV.Tests/Tests.cs index 17827f8..93599be 100644 --- a/CloudFabric.EAV.Tests/Tests.cs +++ b/CloudFabric.EAV.Tests/Tests.cs @@ -23,11 +23,13 @@ using CloudFabric.Projections.InMemory; using CloudFabric.Projections.Postgresql; using CloudFabric.Projections.Queries; +using CloudFabric.Projections.Worker; using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; // ReSharper disable AsyncConverter.ConfigureAwaitHighlighting @@ -39,10 +41,11 @@ public class Tests { private AggregateRepositoryFactory _aggregateRepositoryFactory; - private EAVService _eavService; + private EAVEntityInstanceService _eavEntityInstanceService; private IEventStore _eventStore; - private ILogger _logger; + private IStore _store; + private ILogger _eiLogger; private PostgresqlProjectionRepositoryFactory _projectionRepositoryFactory; private IMapper _mapper; @@ -50,11 +53,11 @@ public class Tests public async Task SetUp() { var loggerFactory = new LoggerFactory(); - _logger = loggerFactory.CreateLogger(); + _eiLogger = loggerFactory.CreateLogger(); var configuration = new MapperConfiguration(cfg => { - cfg.AddMaps(Assembly.GetAssembly(typeof(EAVService))); + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVEntityInstanceService))); } ); _mapper = configuration.CreateMapper(); @@ -67,36 +70,69 @@ public async Task SetUp() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + await _eventStore.Initialize(); _aggregateRepositoryFactory = new AggregateRepositoryFactory(_eventStore); - _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(connectionString); + _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(new LoggerFactory(), connectionString); // Projections engine - takes events from events observer and passes them to multiple projection builders - var projectionsEngine = new ProjectionsEngine( - _projectionRepositoryFactory.GetProjectionRepository() - ); + var projectionsEngine = new ProjectionsEngine(); projectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder = new AttributeConfigurationProjectionBuilder( - _projectionRepositoryFactory, _aggregateRepositoryFactory + _projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); var ordersListProjectionBuilder = new EntityConfigurationProjectionBuilder( - _projectionRepositoryFactory, _aggregateRepositoryFactory + _projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); projectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder); projectionsEngine.AddProjectionBuilder(ordersListProjectionBuilder); + var ProjectionsRebuildProcessor = new ProjectionsRebuildProcessor( + _projectionRepositoryFactory.GetProjectionsIndexStateRepository(), + async (string connectionId) => + { + var rebuildProjectionsEngine = new ProjectionsEngine(); + rebuildProjectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); - await projectionsEngine.StartAsync("TestInstance"); + var attributeConfigurationProjectionBuilder2 = new AttributeConfigurationProjectionBuilder( + _projectionRepositoryFactory, ProjectionOperationIndexSelector.ProjectionRebuild + ); + + var ordersListProjectionBuilder2 = new EntityConfigurationProjectionBuilder( + _projectionRepositoryFactory, ProjectionOperationIndexSelector.ProjectionRebuild + ); + + + rebuildProjectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder2); + rebuildProjectionsEngine.AddProjectionBuilder(ordersListProjectionBuilder2); + + return rebuildProjectionsEngine; + }, + NullLogger.Instance + ); + var attributeConfigurationProjectionRepository = + _projectionRepositoryFactory.GetProjectionRepository(); + await attributeConfigurationProjectionRepository.EnsureIndex(); - _eavService = new EAVService( - _logger, + var entityConfigurationProjectionRepository = + _projectionRepositoryFactory.GetProjectionRepository(); + await entityConfigurationProjectionRepository.EnsureIndex(); + + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + + await projectionsEngine.StartAsync("TestInstance"); + + _eavEntityInstanceService = new EAVEntityInstanceService( + _eiLogger, _mapper, new JsonSerializerOptions() { @@ -109,7 +145,9 @@ public async Task SetUp() ); } + [TestCleanup] + [TestInitialize] public async Task Cleanup() { await _eventStore.DeleteAll(); @@ -131,8 +169,9 @@ public async Task Cleanup() GetProjectionRebuildStateRepository(); await rebuildStateRepository.DeleteAll(); } - catch + catch(Exception ex) { + Console.WriteLine("Failed to clear projection repository {0} {1}", ex.Message, ex.StackTrace); } } @@ -143,19 +182,19 @@ public async Task CreateInstance_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); EntityConfigurationViewModel configuration = - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); validationErrors.Should().BeNull(); createdInstance.Id.Should().NotBeEmpty(); @@ -168,7 +207,7 @@ public async Task CreateInstance_InvalidConfigurationId() EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(Guid.NewGuid()); (EntityInstanceViewModel result, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); result.Should().BeNull(); validationErrors.Should().BeOfType(); validationErrors.As().Errors.Should().ContainKey("EntityConfigurationId"); @@ -182,20 +221,20 @@ public async Task CreateInstance_MissingRequiredAttribute() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); EntityConfigurationViewModel configuration = - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); var requiredAttributeMachineName = "players_min"; EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); entityInstanceCreateRequest.Attributes = entityInstanceCreateRequest.Attributes .Where(a => a.ConfigurationAttributeMachineName != requiredAttributeMachineName).ToList(); (EntityInstanceViewModel createdInstance, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); createdInstance.Should().BeNull(); validationErrors.As().Errors.Should().ContainKey(requiredAttributeMachineName); @@ -224,13 +263,13 @@ public async Task CreateInstance_MissingRequiredAttributeValue() } } }); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); EntityConfigurationViewModel configuration = - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); entityInstanceCreateRequest.Attributes.Add( @@ -241,7 +280,7 @@ public async Task CreateInstance_MissingRequiredAttributeValue() }); (EntityInstanceViewModel createdInstance, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); createdInstance.Should().BeNull(); } @@ -266,12 +305,12 @@ public async Task CreateInstance_IgnoreRequiredCheck_Success() } } }); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); entityInstanceCreateRequest.Attributes.Add( @@ -282,7 +321,7 @@ public async Task CreateInstance_IgnoreRequiredCheck_Success() }); (EntityInstanceViewModel? createdInstance, ProblemDetails? validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest, requiredAttributesCanBeNull: true); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest, requiredAttributesCanBeNull: true); validationErrors.Should().BeNull(); createdInstance.Should().NotBeNull(); } @@ -293,7 +332,7 @@ public async Task CreateEntityConfiguration_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfiguration, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -311,7 +350,7 @@ await _eavService.CreateEntityConfiguration( createdConfiguration.Attributes.Count.Should().Be(configurationCreateRequest.Attributes.Count); var configurationWithAttributes = - await _eavService.GetEntityConfigurationWithAttributes(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfigurationWithAttributes(createdConfiguration.Id); configurationWithAttributes.Should().BeEquivalentTo(configurationCreateRequest); } @@ -323,7 +362,7 @@ public async Task CreateEntityConfiguration_ValidationError() EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); configurationCreateRequest.Name = new List(); (EntityConfigurationViewModel? createdConfiguration, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -354,7 +393,7 @@ public async Task CreateEntityConfiguration_AttributesMachineNamesAreNotUnique() ); (EntityConfigurationViewModel? entityConfig, ProblemDetails? error) = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -372,12 +411,12 @@ public async Task GetEntityConfiguration_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -417,7 +456,7 @@ public async Task UpdateAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); // update added attribute @@ -426,7 +465,7 @@ public async Task UpdateAttribute_Success() numberAttribute.MinimumValue = 0; numberAttribute.MaximumValue = 50; - (AttributeConfigurationViewModel? _, ProblemDetails? error) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? _, ProblemDetails? error) = await _eavEntityInstanceService.UpdateAttribute( created.Attributes[0].AttributeConfigurationId, numberAttribute, CancellationToken.None @@ -434,7 +473,7 @@ public async Task UpdateAttribute_Success() error.Should().BeNull(); - AttributeConfigurationViewModel updatedAttribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel updatedAttribute = await _eavEntityInstanceService.GetAttribute( created.Attributes[0].AttributeConfigurationId, CancellationToken.None ); @@ -480,14 +519,14 @@ public async Task UpdateAttribute_ValidationError() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); // update added attribute numberAttribute.Name = new List(); (AttributeConfigurationViewModel? updatedResult, ProblemDetails? errors) = - await _eavService.UpdateAttribute( + await _eavEntityInstanceService.UpdateAttribute( created.Attributes[0].AttributeConfigurationId, numberAttribute, CancellationToken.None @@ -506,21 +545,21 @@ public async Task DeleteAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel entityConfig, ProblemDetails? _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); Guid attributeToDelete = entityConfig.Attributes.Select(x => x.AttributeConfigurationId).FirstOrDefault(); - await _eavService.DeleteAttributes(new List { attributeToDelete }, CancellationToken.None); + await _eavEntityInstanceService.DeleteAttributes(new List { attributeToDelete }, CancellationToken.None); EntityConfigurationViewModel entityConfAfterAttributeDeleted = - await _eavService.GetEntityConfiguration(entityConfig.Id); + await _eavEntityInstanceService.GetEntityConfiguration(entityConfig.Id); entityConfAfterAttributeDeleted.Attributes.Count().Should().Be(entityConfig.Attributes.Count() - 1); - Func act = async () => await _eavService.GetAttribute(attributeToDelete); + Func act = async () => await _eavEntityInstanceService.GetAttribute(attributeToDelete); await act.Should().ThrowAsync(); ProjectionQueryResult attributesProjections = - await _eavService.ListAttributes(new ProjectionQuery + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Filters = new List { @@ -540,7 +579,7 @@ await _eavService.ListAttributes(new ProjectionQuery public async Task DeleteEntityAttributeFromEntity_EntityNotFound() { Func act = async () => - await _eavService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, + await _eavEntityInstanceService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, Guid.NewGuid(), CancellationToken.None ); @@ -553,13 +592,13 @@ public async Task DeleteEntityAttributeFromEntity_DeleteNotExistingAttribute() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel entityConfig, ProblemDetails? _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); - await _eavService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, + await _eavEntityInstanceService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, entityConfig.Id, CancellationToken.None ); - EntityConfigurationViewModel entityConfigAfterDeletingNotExistingAttribute = await _eavService.GetEntityConfiguration(entityConfig.Id); + EntityConfigurationViewModel entityConfigAfterDeletingNotExistingAttribute = await _eavEntityInstanceService.GetEntityConfiguration(entityConfig.Id); entityConfigAfterDeletingNotExistingAttribute.Attributes.Count.Should().Be(entityConfig.Attributes.Count); } @@ -583,7 +622,7 @@ public async Task GetAttributeByUsedEntities_Success() MaximumValue = 100, MinimumValue = -100 }; - (AttributeConfigurationViewModel numberAttribute, _) = await _eavService.CreateAttribute(numberAttributeRequest); + (AttributeConfigurationViewModel numberAttribute, _) = await _eavEntityInstanceService.CreateAttribute(numberAttributeRequest); var textAttributeRequest = new TextAttributeConfigurationCreateUpdateRequest { @@ -597,7 +636,7 @@ public async Task GetAttributeByUsedEntities_Success() MaxLength = 100, DefaultValue = "-" }; - (AttributeConfigurationViewModel textAttribute, _) = await _eavService.CreateAttribute(textAttributeRequest); + (AttributeConfigurationViewModel textAttribute, _) = await _eavEntityInstanceService.CreateAttribute(textAttributeRequest); // Create entity and add attributes var configurationCreateRequest = new EntityConfigurationCreateRequest @@ -609,10 +648,10 @@ public async Task GetAttributeByUsedEntities_Success() } }; (EntityConfigurationViewModel? createdFirstEntity, _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); - await _eavService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdFirstEntity.Id); - await _eavService.AddAttributeToEntityConfiguration(textAttribute.Id, createdFirstEntity.Id); + await _eavEntityInstanceService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdFirstEntity.Id); + await _eavEntityInstanceService.AddAttributeToEntityConfiguration(textAttribute.Id, createdFirstEntity.Id); // Get attributes by UsedByEntityConfigurationIds ProjectionQuery query = new ProjectionQuery() @@ -644,8 +683,8 @@ public async Task GetAttributeByUsedEntities_Success() } }; (EntityConfigurationViewModel? createdSecondEntity, _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); - await _eavService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdSecondEntity.Id); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdSecondEntity.Id); // Get attribute by one of UsedByEntityConfigurationIds query.Filters = new() @@ -663,7 +702,7 @@ public async Task GetAttributeByUsedEntities_Success() result.Records.FirstOrDefault().Document.UsedByEntityConfigurationIds.Count.Should().Be(2); // Get after delete - await _eavService.DeleteAttributes(new List() { numberAttribute.Id }); + await _eavEntityInstanceService.DeleteAttributes(new List() { numberAttribute.Id }); result = await projectionRepository.Query(query); result.TotalRecordsFound.Should().Be(0); @@ -695,7 +734,7 @@ public async Task GetAttributeByUsedEntities_Success() Attributes = createAttrList }; (EntityConfigurationViewModel? createdThirdEntity, _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); query.Filters = new() { @@ -781,7 +820,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_MachineNamesAreNot (configRequest.Attributes[0] as AttributeConfigurationCreateUpdateRequest)!.MachineName!; (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); var newAttributeRequest = new NumberAttributeConfigurationCreateUpdateRequest { @@ -806,7 +845,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_MachineNamesAreNot }; (EntityConfigurationViewModel? entityConfig, ProblemDetails? error) = - await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); entityConfig.Should().BeNull(); error.Should().NotBeNull(); error.Should().BeOfType(); @@ -822,7 +861,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_Success() EntityConfigurationCreateRequest configRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); const string newAttributeMachineName = "avg_time_mins"; var newAttributeRequest = new NumberAttributeConfigurationCreateUpdateRequest @@ -847,7 +886,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_Success() Name = configRequest.Name }; - _ = await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + _ = await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); //var newAttrIndex = updatedConfig.Attributes.FindIndex(a => a.MachineName == newAttributeMachineName); //newAttrIndex.Should().BePositive(); //var newAttribute = updatedConfig.Attributes[newAttrIndex]; @@ -863,7 +902,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_ValidationError() EntityConfigurationCreateRequest configRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); const string newAttributeMachineName = "some_new_attribute_name"; var newAttributeRequest = new NumberAttributeConfigurationCreateUpdateRequest @@ -891,7 +930,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_ValidationError() }; (_, ProblemDetails? errors) = - await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); errors.Should().NotBeNull(); errors.As().Errors.Should().ContainKey(newAttributeMachineName); errors.As().Errors[newAttributeMachineName].Should() @@ -906,7 +945,7 @@ public async Task UpdateEntityConfiguration_ChangeAttributeName_Fail() EntityConfigurationCreateRequest configRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); var newNameRequest = new List { new() { CultureInfoId = cultureId, String = newName } @@ -919,7 +958,7 @@ public async Task UpdateEntityConfiguration_ChangeAttributeName_Fail() Name = configRequest.Name }; (EntityConfigurationViewModel? updatedConfig, ProblemDetails? updateError) = - await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); updateError.Should().NotBeNull(); updateError.As().Errors.First().Key.Should().Be("Attributes[0]"); @@ -930,7 +969,7 @@ public async Task UpdateEntityConfiguration_ChangeAttributeName_Fail() public async Task EntityConfigurationProjectionCreated() { ProjectionQueryResult configurationItemsStart = - await _eavService.ListEntityConfigurations( + await _eavEntityInstanceService.ListEntityConfigurations( ProjectionQueryExpressionExtensions.Where(x => x.MachineName == "BoardGame" ), @@ -943,14 +982,14 @@ await _eavService.ListEntityConfigurations( EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel?, ProblemDetails?) createdConfiguration = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); // verify projection is created ProjectionQueryResult configurationItems = - await _eavService.ListEntityConfigurations( + await _eavEntityInstanceService.ListEntityConfigurations( ProjectionQueryExpressionExtensions.Where(x => x.MachineName == "BoardGame" ) @@ -967,19 +1006,19 @@ public async Task GetEntityConfigurationProjectionByTenantId_Success() EntityConfigurationCreateRequest configurationCreateRequest2 = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration1, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration1, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest1, CancellationToken.None ); - (EntityConfigurationViewModel? createdConfiguration2, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration2, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest2, CancellationToken.None ); // verify projection is created ProjectionQueryResult configurationItems = - await _eavService.ListEntityConfigurations( + await _eavEntityInstanceService.ListEntityConfigurations( ProjectionQueryExpressionExtensions.Where(x => x.TenantId == createdConfiguration2.TenantId ) @@ -1016,11 +1055,11 @@ public async Task CreateTextAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.MachineName.Should().Be(textAttrbiuteRequest.MachineName); @@ -1057,14 +1096,14 @@ public async Task CreateTextAttribute_MaxLengthValidationError() }; // check - (_, ProblemDetails? errors) = await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + (_, ProblemDetails? errors) = await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); errors.As().Errors.Count.Should().Be(1); errors.As().Errors.First().Value.First().Should().Be("Max length can't be negative or zero"); - // check for negative max length + // check for negative max length textAttrbiuteRequest.MaxLength = -10; - (_, errors) = await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + (_, errors) = await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); errors.As().Errors.First().Value.First().Should().NotBeNullOrEmpty(); } @@ -1094,7 +1133,7 @@ public async Task CreateTextAttribute_DefaultValueValidationError() Attributes = new List { textAttrbiuteRequest } }; - (_, ProblemDetails? errors) = await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + (_, ProblemDetails? errors) = await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); errors.As().Errors .First().Value.First().Should().Be("Default value length cannot be greater than MaxLength"); @@ -1129,11 +1168,11 @@ public async Task CreateNumberAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(numberAttribute.Name); @@ -1145,7 +1184,7 @@ public async Task CreateNumberAttribute_ValidationError() var request = new NumberAttributeConfigurationCreateUpdateRequest { MachineName = "avg_time_mins" }; (AttributeConfigurationViewModel? result, ValidationErrorResponse? errors) = - await _eavService.CreateAttribute(request); + await _eavEntityInstanceService.CreateAttribute(request); result.Should().BeNull(); errors.Should().NotBeNull(); errors.As().Errors.Should().ContainKey(request.MachineName); @@ -1183,11 +1222,11 @@ public async Task CreateMoneyAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(moneyAttribute.Name); @@ -1233,11 +1272,11 @@ public async Task CreateMoneyAttributeCustomCurrency_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(moneyAttribute.Name); @@ -1274,7 +1313,7 @@ public async Task CreateMoneyAttribute_InvalidDefaultId() }; (EntityConfigurationViewModel? created, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Should().BeNull(); errors.Should().NotBeNull(); } @@ -1311,7 +1350,7 @@ public async Task CreateMoneyAttribute_EmptyList() }; (EntityConfigurationViewModel? created, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Should().BeNull(); errors.Should().NotBeNull(); } @@ -1347,10 +1386,10 @@ public async Task CreateFileAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); - AttributeConfigurationViewModel createdAttribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel createdAttribute = await _eavEntityInstanceService.GetAttribute( created.Attributes[0].AttributeConfigurationId, CancellationToken.None ); @@ -1386,19 +1425,19 @@ public async Task UpdateFileAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created!.Attributes.Count.Should().Be(1); fileAttribute.IsDownloadable = false; fileAttribute.IsRequired = false; - (AttributeConfigurationViewModel? updated, _) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? updated, _) = await _eavEntityInstanceService.UpdateAttribute( created.Attributes[0].AttributeConfigurationId, fileAttribute, CancellationToken.None ); - AttributeConfigurationViewModel createdAttribute = await _eavService + AttributeConfigurationViewModel createdAttribute = await _eavEntityInstanceService .GetAttribute(updated!.Id, CancellationToken.None); createdAttribute.IsRequired.Should().Be(fileAttribute.IsRequired); @@ -1432,7 +1471,7 @@ public async Task CreateFileAttributeInstance_Success() }; (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); createdConfig!.Attributes.Count.Should().Be(1); var instanceRequest = new EntityInstanceCreateRequest @@ -1453,11 +1492,11 @@ public async Task CreateFileAttributeInstance_Success() }; (EntityInstanceViewModel createdInstance, ProblemDetails _) = - await _eavService.CreateEntityInstance(instanceRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceRequest); createdInstance.Should().NotBeNull(); - createdInstance = await _eavService.GetEntityInstance(createdInstance.Id, createdConfig.Id.ToString()); + createdInstance = await _eavEntityInstanceService.GetEntityInstance(createdInstance.Id, createdConfig.Id.ToString()); createdInstance.Attributes.Count.Should().Be(1); createdInstance.Attributes[0] .As() @@ -1500,10 +1539,10 @@ public async Task GetNumberAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); - AttributeConfigurationViewModel createdAttribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel createdAttribute = await _eavEntityInstanceService.GetAttribute( created.Attributes[0].AttributeConfigurationId ); @@ -1549,11 +1588,11 @@ public async Task CreateValueFromListAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); created!.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(valueFromListAttribute.Name); @@ -1601,7 +1640,7 @@ public async Task CreateValueFromListAttribute_ValidationError() // case check repeated name (EntityConfigurationViewModel entity, ProblemDetails errors) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); entity.Should().BeNull(); errors.Should().BeOfType(); @@ -1615,7 +1654,7 @@ public async Task CreateValueFromListAttribute_ValidationError() }; (entity, errors) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); entity.Should().BeNull(); errors.Should().BeOfType(); @@ -1626,7 +1665,7 @@ public async Task CreateValueFromListAttribute_ValidationError() valueFromListAttribute.ValuesList = new List(); (entity, errors) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); entity.Should().BeNull(); errors.Should().BeOfType(); @@ -1661,7 +1700,7 @@ public async Task UpdateValueFromListAttribute_Success() }; (AttributeConfigurationViewModel? valueFromListAttribute, _) = - await _eavService.CreateAttribute(valueFromListAttributeCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(valueFromListAttributeCreateRequest, CancellationToken.None); // create request with changed properties and update attribute @@ -1672,7 +1711,7 @@ public async Task UpdateValueFromListAttribute_Success() }; (AttributeConfigurationViewModel? changedAttribute, _) = - await _eavService.UpdateAttribute(valueFromListAttribute.Id, + await _eavEntityInstanceService.UpdateAttribute(valueFromListAttribute.Id, valueFromListAttributeCreateRequest!, CancellationToken.None ); @@ -1718,10 +1757,10 @@ public async Task CreateEntityInstanceWithValueFromListAttribute_ValidationError }; (EntityConfigurationViewModel? entityConfiguration, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); // create entity instance using wrong type of attribute - (EntityInstanceViewModel result, ProblemDetails validationErrors) = await _eavService.CreateEntityInstance( + (EntityInstanceViewModel result, ProblemDetails validationErrors) = await _eavEntityInstanceService.CreateEntityInstance( new EntityInstanceCreateRequest { EntityConfigurationId = entityConfiguration.Id, @@ -1740,7 +1779,7 @@ public async Task CreateEntityInstanceWithValueFromListAttribute_ValidationError validationErrors.As().Errors["testValueAttr"].First().Should() .Be("Cannot validate attribute. Expected attribute type: Value from list"); - (result, validationErrors) = await _eavService.CreateEntityInstance(new EntityInstanceCreateRequest + (result, validationErrors) = await _eavEntityInstanceService.CreateEntityInstance(new EntityInstanceCreateRequest { EntityConfigurationId = entityConfiguration.Id, Attributes = new List @@ -1794,11 +1833,11 @@ public async Task CreateSerialAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); created!.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(serialAttributeCreateRequest.Name); @@ -1840,13 +1879,13 @@ public async Task CreateSerialAttribute_ValidationError() }; (AttributeConfigurationViewModel _, ValidationErrorResponse errors) = - await _eavService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); errors.Should().BeOfType(); errors.Errors.Should().Contain(x => x.Value.Contains("Increment value must not be negative or 0")); errors.Errors.Should().Contain(x => x.Value.Contains("Statring number must not be negative")); serialAttributeCreateRequest.Increment = -1; - (_, errors) = await _eavService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); + (_, errors) = await _eavEntityInstanceService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); errors.Errors.Should().Contain(x => x.Value.Contains("Increment value must not be negative or 0")); } @@ -1885,7 +1924,7 @@ public async Task UpdateSerialAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); // create update request and change attribute var serialAttributeUpdateRequest = new SerialAttributeConfigurationUpdateRequest @@ -1904,7 +1943,7 @@ public async Task UpdateSerialAttribute_Success() Increment = 5 }; - (AttributeConfigurationViewModel? updatedAttribute, ProblemDetails _) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? updatedAttribute, ProblemDetails _) = await _eavEntityInstanceService.UpdateAttribute( created.Attributes.FirstOrDefault().AttributeConfigurationId, serialAttributeUpdateRequest, CancellationToken.None @@ -1961,9 +2000,9 @@ public async Task CreateEntityInstanceWithSerialAttributes_Success() }; (EntityConfigurationViewModel? entityConfig, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); - (EntityInstanceViewModel result, ProblemDetails _) = await _eavService.CreateEntityInstance( + (EntityInstanceViewModel result, ProblemDetails _) = await _eavEntityInstanceService.CreateEntityInstance( new EntityInstanceCreateRequest { EntityConfigurationId = entityConfig.Id, @@ -2003,7 +2042,7 @@ public async Task CreateEntityInstanceWithSerialAttributes_Success() .As().Value.Should().Be(serialAttributeCreateRequest.StartingNumber); // create another entity instance - (result, _) = await _eavService.CreateEntityInstance(new EntityInstanceCreateRequest + (result, _) = await _eavEntityInstanceService.CreateEntityInstance(new EntityInstanceCreateRequest { EntityConfigurationId = entityConfig.Id, Attributes = new List @@ -2057,7 +2096,7 @@ public async Task AddAttributeToEntityConfiguration_Success() }; (EntityConfigurationViewModel? createdEntityConfiguration, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); var numberAttribute = new NumberAttributeConfigurationCreateUpdateRequest { @@ -2078,17 +2117,17 @@ public async Task AddAttributeToEntityConfiguration_Success() }; (AttributeConfigurationViewModel? createdAttribute, _) = - await _eavService.CreateAttribute(numberAttribute, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(numberAttribute, CancellationToken.None); createdAttribute.Should().NotBeNull(); - await _eavService.AddAttributeToEntityConfiguration( + await _eavEntityInstanceService.AddAttributeToEntityConfiguration( createdAttribute.Id, createdEntityConfiguration.Id, CancellationToken.None ); // check that attribute is added - EntityConfigurationViewModel updatedEntityConfiguration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel updatedEntityConfiguration = await _eavEntityInstanceService.GetEntityConfiguration( createdEntityConfiguration.Id ); @@ -2108,7 +2147,7 @@ public async Task AddAttributeToEntityConfiguration_MachineNamesAreNotUnique() (configCreateRequest.Attributes[0] as AttributeConfigurationCreateUpdateRequest)!.MachineName!; (EntityConfigurationViewModel? createdEntityConfiguration, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); var numberAttribute = new NumberAttributeConfigurationCreateUpdateRequest { @@ -2129,11 +2168,11 @@ public async Task AddAttributeToEntityConfiguration_MachineNamesAreNotUnique() }; (AttributeConfigurationViewModel? createdAttribute, _) = - await _eavService.CreateAttribute(numberAttribute, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(numberAttribute, CancellationToken.None); createdAttribute.Should().NotBeNull(); (EntityConfigurationViewModel? entityConfig, ProblemDetails? error) = - await _eavService.AddAttributeToEntityConfiguration( + await _eavEntityInstanceService.AddAttributeToEntityConfiguration( createdAttribute.Id, createdEntityConfiguration.Id, CancellationToken.None @@ -2153,7 +2192,7 @@ public async Task UpdateInstance_UpdateAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2163,7 +2202,7 @@ public async Task UpdateInstance_UpdateAttribute_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); var playerMaxIndex = attributesRequest.FindIndex(a => a.ConfigurationAttributeMachineName == changedAttributeName); @@ -2180,7 +2219,7 @@ public async Task UpdateInstance_UpdateAttribute_Success() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(),updateRequest); + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(),updateRequest); updatedInstance.Attributes.First(a => a.ConfigurationAttributeMachineName == changedAttributeName) .As().Value.Should().Be(10); } @@ -2192,7 +2231,7 @@ public async Task UpdateInstance_UpdateAttribute_FailValidation() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2202,7 +2241,7 @@ public async Task UpdateInstance_UpdateAttribute_FailValidation() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); var playerMaxIndex = attributesRequest.FindIndex(a => a.ConfigurationAttributeMachineName == changedAttributeName); @@ -2219,7 +2258,7 @@ public async Task UpdateInstance_UpdateAttribute_FailValidation() }; (EntityInstanceViewModel updatedInstance, ProblemDetails validationErrors) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); updatedInstance.Should().BeNull(); validationErrors.As().Errors.Should().ContainKey(changedAttributeName); } @@ -2231,7 +2270,7 @@ public async Task UpdateInstance_AddAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2241,7 +2280,7 @@ public async Task UpdateInstance_AddAttribute_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest.Add(new NumberAttributeInstanceCreateUpdateRequest { @@ -2258,7 +2297,7 @@ public async Task UpdateInstance_AddAttribute_Success() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); updatedInstance.Attributes.First(a => a.ConfigurationAttributeMachineName == changedAttributeName) .As().Value.Should().Be(30); } @@ -2270,7 +2309,7 @@ public async Task CreateInstance_NumberOfItemsWithAttributeUpdated_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2280,7 +2319,7 @@ public async Task CreateInstance_NumberOfItemsWithAttributeUpdated_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest.Add(new NumberAttributeInstanceCreateUpdateRequest { @@ -2296,10 +2335,10 @@ public async Task CreateInstance_NumberOfItemsWithAttributeUpdated_Success() Id = createdInstance.Id }; - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); ProjectionQueryResult attributeConfigurations = - await _eavService.ListAttributes( + await _eavEntityInstanceService.ListAttributes( new ProjectionQuery { Filters = new List @@ -2333,7 +2372,7 @@ public async Task UpdateInstance_AddNumberAttribute_InvalidNumberType() (numberAttributeConfig as NumberAttributeConfigurationCreateUpdateRequest)!.NumberType = NumberAttributeType.Integer; - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2351,7 +2390,7 @@ public async Task UpdateInstance_AddNumberAttribute_InvalidNumberType() ); (EntityInstanceViewModel instance, ProblemDetails error) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); instance.Should().BeNull(); error.As().Errors.Should() .Contain(x => x.Value.Contains("Value is not an integer value")); @@ -2364,7 +2403,7 @@ public async Task UpdateInstance_AddAttribute_IgnoreAttributeNotInConfig() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2374,7 +2413,7 @@ public async Task UpdateInstance_AddAttribute_IgnoreAttributeNotInConfig() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest.Add(new NumberAttributeInstanceCreateUpdateRequest { @@ -2391,7 +2430,7 @@ public async Task UpdateInstance_AddAttribute_IgnoreAttributeNotInConfig() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); updatedInstance.Attributes.FirstOrDefault(a => a.ConfigurationAttributeMachineName == changedAttributeName) .Should().BeNull(); } @@ -2403,7 +2442,7 @@ public async Task UpdateInstance_RemoveAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2413,7 +2452,7 @@ public async Task UpdateInstance_RemoveAttribute_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); var updateRequest = new EntityInstanceUpdateRequest { @@ -2427,7 +2466,7 @@ public async Task UpdateInstance_RemoveAttribute_Success() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); updatedInstance.Attributes.FirstOrDefault(a => a.ConfigurationAttributeMachineName == changedAttributeName) .Should().BeNull(); } @@ -2439,7 +2478,7 @@ public async Task UpdateInstance_RemoveAttribute_FailValidation() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2449,7 +2488,7 @@ public async Task UpdateInstance_RemoveAttribute_FailValidation() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest = attributesRequest .Where(a => a.ConfigurationAttributeMachineName != changedAttributeName).ToList(); @@ -2465,7 +2504,7 @@ public async Task UpdateInstance_RemoveAttribute_FailValidation() }; (EntityInstanceViewModel updatedInstance, ProblemDetails errors) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest); updatedInstance.Should().BeNull(); errors.As().Errors.Should().ContainKey(changedAttributeName); } @@ -2503,7 +2542,7 @@ public async Task UpdateInstanceSerialAttributeExternalValue_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); // create entity instance for further update var entityInstanceCreateRequest = new EntityInstanceCreateRequest @@ -2520,7 +2559,7 @@ public async Task UpdateInstanceSerialAttributeExternalValue_Success() } } }; - (EntityInstanceViewModel createdItem, _) = await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + (EntityInstanceViewModel createdItem, _) = await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); // update entity instance var updateSerialInstanceRequest = new SerialAttributeInstanceCreateUpdateRequest @@ -2537,7 +2576,7 @@ public async Task UpdateInstanceSerialAttributeExternalValue_Success() Id = createdItem.Id, }; - (EntityInstanceViewModel updatedinstance, _) = await _eavService.UpdateEntityInstance( + (EntityInstanceViewModel updatedinstance, _) = await _eavEntityInstanceService.UpdateEntityInstance( createdItem.EntityConfigurationId.ToString(), entityInstanceUpdateRequest ); @@ -2579,7 +2618,7 @@ public async Task UpdateInstanceSerialExternalValue_WrongValue() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); // create entity instance for further update var entityInstanceCreateRequest = new EntityInstanceCreateRequest @@ -2596,7 +2635,7 @@ public async Task UpdateInstanceSerialExternalValue_WrongValue() } } }; - (EntityInstanceViewModel? createdItem, _) = await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + (EntityInstanceViewModel? createdItem, _) = await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); // update entity instance var updateSerialInstanceRequest = new SerialAttributeInstanceCreateUpdateRequest @@ -2613,21 +2652,21 @@ public async Task UpdateInstanceSerialExternalValue_WrongValue() Id = createdItem.Id, }; - (_, ProblemDetails updateErrors) = await _eavService.UpdateEntityInstance( + (_, ProblemDetails updateErrors) = await _eavEntityInstanceService.UpdateEntityInstance( createdItem.EntityConfigurationId.ToString(), entityInstanceUpdateRequest ); updateErrors.Should().NotBeNull(); updateSerialInstanceRequest.Value = updateSerialInstanceRequest.Value - DateTime.UtcNow.Ticks; - (_, updateErrors) = await _eavService.UpdateEntityInstance( + (_, updateErrors) = await _eavEntityInstanceService.UpdateEntityInstance( createdItem.EntityConfigurationId.ToString(), entityInstanceUpdateRequest ); updateErrors.Should().NotBeNull(); updateSerialInstanceRequest.Value = null; - (_, updateErrors) = await _eavService.UpdateEntityInstance( + (_, updateErrors) = await _eavEntityInstanceService.UpdateEntityInstance( createdItem.EntityConfigurationId.ToString(), entityInstanceUpdateRequest ); @@ -2655,7 +2694,7 @@ public async Task CreateNumberAttributeAsReference_Success() (AttributeConfigurationViewModel? priceAttributeCreated, _) = - await _eavService.CreateAttribute(priceAttribute, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(priceAttribute, CancellationToken.None); var entityConfigurationCreateRequest = new EntityConfigurationCreateRequest { @@ -2681,13 +2720,13 @@ public async Task CreateNumberAttributeAsReference_Success() } }; - _ = await _eavService.CreateEntityConfiguration( + _ = await _eavEntityInstanceService.CreateEntityConfiguration( entityConfigurationCreateRequest, CancellationToken.None ); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 1000 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 1000 }); allAttributes.Records.Count.Should().Be(2); } @@ -2697,12 +2736,12 @@ public async Task CreateInstanceAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -2712,7 +2751,7 @@ public async Task CreateInstanceAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance(instanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); @@ -2724,7 +2763,7 @@ public async Task CreateInstanceAndQuery() Filters = new List { new("Id", FilterOperator.Equal, createdInstance.Id) } }; - await _eavService + await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); } @@ -2841,10 +2880,10 @@ public async Task AddAttributeMetadata_Success() (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); createdConfig.Should().NotBeNull(); - AttributeConfigurationViewModel attribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel attribute = await _eavEntityInstanceService.GetAttribute( createdConfig!.Attributes[0].AttributeConfigurationId, CancellationToken.None ); @@ -2855,7 +2894,7 @@ public async Task AddAttributeMetadata_Success() // check projections ProjectionQueryResult attributes = - await _eavService.ListAttributes(new ProjectionQuery()); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery()); attributes.Records.First().Document!.Metadata.Should().Be(attribute.Metadata); } @@ -2891,12 +2930,12 @@ public async Task UpdateAttributeMetadata_Success() (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); createdConfig.Should().NotBeNull(); // update attribute metadata priceAttribute.Metadata = "updated metadata"; - (AttributeConfigurationViewModel? updatedAttribute, _) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? updatedAttribute, _) = await _eavEntityInstanceService.UpdateAttribute( createdConfig.Attributes[0].AttributeConfigurationId, priceAttribute, CancellationToken.None @@ -2904,7 +2943,7 @@ public async Task UpdateAttributeMetadata_Success() updatedAttribute.Should().NotBeNull(); - AttributeConfigurationViewModel attribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel attribute = await _eavEntityInstanceService.GetAttribute( updatedAttribute.Id, CancellationToken.None ); @@ -2913,7 +2952,7 @@ public async Task UpdateAttributeMetadata_Success() // check projections ProjectionQueryResult attributesList = - await _eavService.ListAttributes(new ProjectionQuery()); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery()); attributesList.Records.First().Document!.Metadata.Should().Be(priceAttribute.Metadata); } @@ -2998,13 +3037,13 @@ private async Task CreateSimpleArrayOfTypesAttribute_Success(EavAttributeType ty // Act (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); // Assert // Check domain models var createdArrayAttributeRef = created?.Attributes.First(); createdArrayAttributeRef.Should().NotBeNull(); - var createdAttribute = await _eavService.GetAttribute(createdArrayAttributeRef!.AttributeConfigurationId) as ArrayAttributeConfigurationViewModel; + var createdAttribute = await _eavEntityInstanceService.GetAttribute(createdArrayAttributeRef!.AttributeConfigurationId) as ArrayAttributeConfigurationViewModel; createdAttribute.Name.Should().BeEquivalentTo(arrayAttribute.Name); createdAttribute.Description.Should().BeEquivalentTo(arrayAttribute.Description); @@ -3013,7 +3052,7 @@ private async Task CreateSimpleArrayOfTypesAttribute_Success(EavAttributeType ty // Check element config var elementAttributeId = createdAttribute.ItemsAttributeConfigurationId; elementAttributeId.Should().NotBeEmpty(); - var createdElementAttribute = await _eavService.GetAttribute(elementAttributeId); + var createdElementAttribute = await _eavEntityInstanceService.GetAttribute(elementAttributeId); var defaultConfigToCompare = DefaultAttributeConfigurationFactory.GetDefaultConfiguration(type, createdElementAttribute.MachineName, @@ -3026,11 +3065,16 @@ private async Task CreateSimpleArrayOfTypesAttribute_Success(EavAttributeType ty private IProjectionRepository GetProjectionRebuildStateRepository() { - return new InMemoryProjectionRepository(); + return new InMemoryProjectionRepository(new LoggerFactory()); } - private IEventsObserver GetEventStoreEventsObserver() + private EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + var loggerFactory = new LoggerFactory(); + + return new PostgresqlEventStoreEventObserver( + (PostgresqlEventStore)_eventStore, + loggerFactory.CreateLogger() + ); } }