From 0adeaa21eb194cbba0ec2e11c4d3966eb70242a5 Mon Sep 17 00:00:00 2001 From: Scott Arbeit Date: Sun, 26 Nov 2023 01:07:49 -0800 Subject: [PATCH] Bug fixes after processCommand restructuring; other cleanup. --- src/.grace/graceconfig.json | 16 +-- src/Create-Grace-Objects.ps1 | 56 +++++---- src/Grace.Actors/Branch.Actor.fs | 3 +- src/Grace.Actors/Commands.Actor.fs | 1 + src/Grace.Actors/Events.Actor.fs | 1 + src/Grace.Actors/Repository.Actor.fs | 2 + src/Grace.Actors/Services.Actor.fs | 109 ++++++++++++++++-- src/Grace.CLI/Properties/launchSettings.json | 6 +- src/Grace.SDK/Common.SDK.fs | 19 ++- src/Grace.Server/Branch.Server.fs | 105 ++++++++++++----- .../Middleware/ValidateIds.Middleware.fs | 21 +--- src/Grace.Server/Organization.Server.fs | 87 ++++++++++---- src/Grace.Server/Owner.Server.fs | 65 +++++++---- src/Grace.Server/Repository.Server.fs | 109 ++++++++++++++---- src/Grace.Server/Services.Server.fs | 49 +++++--- src/Grace.Server/Startup.Server.fs | 1 + src/Grace.Server/Validations.Server.fs | 87 ++++++++------ .../Resources/Text/Languages.Resources.fs | 2 + src/Grace.Shared/Resources/Text/en-US.fs | 2 + .../Validation/Errors.Validation.fs | 4 + 20 files changed, 526 insertions(+), 219 deletions(-) diff --git a/src/.grace/graceconfig.json b/src/.grace/graceconfig.json index ac6649a..5d8e3b0 100644 --- a/src/.grace/graceconfig.json +++ b/src/.grace/graceconfig.json @@ -1,12 +1,12 @@ { - "OwnerId": "f37d3fcd-5f76-4187-942d-890f21656ebe", - "OwnerName": "Owner9FED", - "OrganizationId": "df1e97e4-5ee9-4e37-b095-4a677b44ef57", - "OrganizationName": "Organization001", - "RepositoryId": "8c4eae5f-f734-4067-a39f-ce2d9c9fabbc", - "RepositoryName": "Repository001", - "BranchId": "de9220fb-e2bc-4562-b5c4-1798d81100c1", - "BranchName": "main", + "OwnerId": "1d0f3f91-a98a-4188-bff5-a2914b52301a", + "OwnerName": "Owner85F5", + "OrganizationId": "80be1fb5-35bb-4f23-b64c-99528d6d920d", + "OrganizationName": "Org85F5", + "RepositoryId": "a81b4530-3d2d-4485-af93-fbfa3c41f0d9", + "RepositoryName": "Repo85F5", + "BranchId": "8cb99933-2c2b-4121-a4d4-67ce6b61cbb9", + "BranchName": "Branch85F5", "DefaultBranchName": "main", "Themes": [ { diff --git a/src/Create-Grace-Objects.ps1 b/src/Create-Grace-Objects.ps1 index 5dfd2e5..015376d 100644 --- a/src/Create-Grace-Objects.ps1 +++ b/src/Create-Grace-Objects.ps1 @@ -1,7 +1,10 @@ $startTime = Get-Date +$words = "Sit fusce at sociosqu eros bibendum aliquet cursus ante non facilisis tempor Scelerisque arcu potenti feugiat fermentum viverra et litora facilisis vestibulum sit aliquam quisque sagittis ut Ultricies nisi urna cursus tellus tempor vivamus nec Dictumst tristique porta vel cubilia mollis Tempus nullam laoreet sit vestibulum etiam in volutpat dui class netus morbi Duis facilisis at aliquet fusce nisi Nulla arcu molestie mauris integer aenean ligula curabitur dui sociosqu suspendisse mi fringilla faucibus Rhoncus habitasse massa amet ipsum ligula quisque Quisque fames bibendum eu ullamcorper pulvinar in aenean hendrerit Augue tristique aenean amet auctor curabitur congue placerat aenean posuere porttitor pulvinar lectus Mattis aenean elit condimentum nam iaculis ante felis sollicitudin Risus viverra ornare curabitur sem massa nibh vulputate senectus dictum vitae leo varius dictumst tristique Ultrices ut blandit adipiscing dictumst sagittis elementum urna Vel feugiat consectetur malesuada nibh turpis odio convallis molestie vulputate magna venenatis lacinia Suscipit consequat lectus nullam suspendisse aliquam sed venenatis Feugiat vehicula iaculis donec aenean Volutpat amet feugiat fringilla bibendum scelerisque fermentum pellentesque hendrerit dapibus primis eu ipsum proin mauris Amet magna non mattis dictum risus sit Luctus hendrerit in integer euismod sapien aenean vel maecenas venenatis lorem cubilia taciti Id mauris dictum aenean leo quisque auctor sagittis nisl rutrum at Iaculis luctus orci egestas metus commodo praesent sodales nam quis conubia cras sagittis vestibulum Viverra justo cursus tempor fringilla egestas Potenti aliquam quisque tincidunt pellentesque Lacinia eu convallis quis risus accumsan Augue adipiscing orci massa lorem curabitur eleifend tincidunt justo varius vulputate Mollis aenean est pulvinar proin in donec bibendum dolor quis sociosqu mattis mi Euismod urna leo mollis potenti fames mattis ultrices diam Vivamus sit mattis vehicula viverra mi imperdiet Adipiscing est vehicula scelerisque velit Malesuada integer quisque fusce quis mollis eros Leo nec tellus curabitur ornare amet quisque fusce habitasse morbi Sem lacinia eu aenean pretium curae dolor cubilia faucibus purus Sollicitudin nisl tempus auctor etiam felis urna consectetur donec dui Posuere elit orci lobortis magna Enim at pellentesque ac taciti convallis sapien ad elit Integer potenti malesuada lacinia fames euismod amet purus justo sociosqu dolor cras tempus dictumst Dictumst adipiscing quisque sapien pharetra pretium aliquam nunc ipsum varius mi justo aenean mattis Aenean conubia felis inceptos nulla ante sociosqu libero non imperdiet Nunc feugiat sodales commodo interdum rhoncus nulla aliquet cras sociosqu eros sed Vivamus varius sapien sollicitudin curabitur class aenean tempus tempor magna donec bibendum nulla morbi semper Praesent inceptos etiam tempus in Varius hac et feugiat nullam dictum vivamus adipiscing ut in eros nulla molestie ante Interdum dictum volutpat accumsan posuere quis amet curae nostra purus fusce nisl lacus Aenean erat suscipit urna ante In ad varius interdum porta at pulvinar aptent enim nam sit ultrices hendrerit Vitae rhoncus consequat non metus nullam augue Massa vestibulum dapibus lectus nibh at tortor ullamcorper mattis rutrum pellentesque aliquam adipiscing porttitor".Split() + 1..1 | ForEach-Object -Parallel { - $words = "Sit fusce at sociosqu eros bibendum aliquet cursus ante non facilisis tempor Scelerisque arcu potenti feugiat fermentum viverra et litora facilisis vestibulum sit aliquam quisque sagittis ut Ultricies nisi urna cursus tellus tempor vivamus nec Dictumst tristique porta vel cubilia mollis Tempus nullam laoreet sit vestibulum etiam in volutpat dui class netus morbi Duis facilisis at aliquet fusce nisi Nulla arcu molestie mauris integer aenean ligula curabitur dui sociosqu suspendisse mi fringilla faucibus Rhoncus habitasse massa amet ipsum ligula quisque Quisque fames bibendum eu ullamcorper pulvinar in aenean hendrerit Augue tristique aenean amet auctor curabitur congue placerat aenean posuere porttitor pulvinar lectus Mattis aenean elit condimentum nam iaculis ante felis sollicitudin Risus viverra ornare curabitur sem massa nibh vulputate senectus dictum vitae leo varius dictumst tristique Ultrices ut blandit adipiscing dictumst sagittis elementum urna Vel feugiat consectetur malesuada nibh turpis odio convallis molestie vulputate magna venenatis lacinia Suscipit consequat lectus nullam suspendisse aliquam sed venenatis Feugiat vehicula iaculis donec aenean Volutpat amet feugiat fringilla bibendum scelerisque fermentum pellentesque hendrerit dapibus primis eu ipsum proin mauris Amet magna non mattis dictum risus sit Luctus hendrerit in integer euismod sapien aenean vel maecenas venenatis lorem cubilia taciti Id mauris dictum aenean leo quisque auctor sagittis nisl rutrum at Iaculis luctus orci egestas metus commodo praesent sodales nam quis conubia cras sagittis vestibulum Viverra justo cursus tempor fringilla egestas Potenti aliquam quisque tincidunt pellentesque Lacinia eu convallis quis risus accumsan Augue adipiscing orci massa lorem curabitur eleifend tincidunt justo varius vulputate Mollis aenean est pulvinar proin in donec bibendum dolor quis sociosqu mattis mi Euismod urna leo mollis potenti fames mattis ultrices diam Vivamus sit mattis vehicula viverra mi imperdiet Adipiscing est vehicula scelerisque velit Malesuada integer quisque fusce quis mollis eros Leo nec tellus curabitur ornare amet quisque fusce habitasse morbi Sem lacinia eu aenean pretium curae dolor cubilia faucibus purus Sollicitudin nisl tempus auctor etiam felis urna consectetur donec dui Posuere elit orci lobortis magna Enim at pellentesque ac taciti convallis sapien ad elit Integer potenti malesuada lacinia fames euismod amet purus justo sociosqu dolor cras tempus dictumst Dictumst adipiscing quisque sapien pharetra pretium aliquam nunc ipsum varius mi justo aenean mattis Aenean conubia felis inceptos nulla ante sociosqu libero non imperdiet Nunc feugiat sodales commodo interdum rhoncus nulla aliquet cras sociosqu eros sed Vivamus varius sapien sollicitudin curabitur class aenean tempus tempor magna donec bibendum nulla morbi semper Praesent inceptos etiam tempus in Varius hac et feugiat nullam dictum vivamus adipiscing ut in eros nulla molestie ante Interdum dictum volutpat accumsan posuere quis amet curae nostra purus fusce nisl lacus Aenean erat suscipit urna ante In ad varius interdum porta at pulvinar aptent enim nam sit ultrices hendrerit Vitae rhoncus consequat non metus nullam augue Massa vestibulum dapibus lectus nibh at tortor ullamcorper mattis rutrum pellentesque aliquam adipiscing porttitor".Split() + Set-Alias -Name grace -Value D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe + $suffix = (Get-Random -Maximum 65536).ToString("X4") $ownerId = (New-Guid).ToString() @@ -16,29 +19,34 @@ $startTime = Get-Date $branchId = (New-Guid).ToString() $branchName = 'Branch' + $suffix - "D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe owner create --output Verbose --ownerName $ownerNameOriginal --ownerId $ownerId" - "D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe owner set-name --output Verbose --ownerName $ownerNameOriginal --newName $ownerName" - "D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe org create --output Verbose --ownerName $ownerName --organizationName $orgNameOriginal --organizationId $organizationId" - "D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe org set-name --output Verbose --ownerName $ownerName --organizationName $orgNameOriginal --newName $orgName" - "D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe repo create --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoNameOriginal --repositoryId $repoId" - "D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe repo set-name --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoNameOriginal --newName $repoName" - "D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe branch create --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName" - - 1..0 | ForEach-Object { - $numberOfWords = Get-Random -Minimum 3 -Maximum 9 - $start = Get-Random -Minimum 0 -Maximum ($words.Count - $numberOfWords) - $message = '' - for ($i = $0; $i -lt $numberOfWords; $i++) { - $message += $words[$i + $start] + " " - } - - switch (Get-Random -Maximum 4) { - 0 {D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe branch save --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName} - 1 {D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe branch checkpoint --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName -m $message} - 2 {D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe branch commit --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName -m $message} - 3 {D:\Source\Grace\src\Grace.CLI\bin\Debug\net8.0\Grace.CLI.exe branch tag --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName -m $message} - } - } + grace owner create --output Verbose --ownerName $ownerNameOriginal --ownerId $ownerId + grace owner set-name --output Verbose --ownerName $ownerNameOriginal --newName $ownerName + grace org create --output Verbose --ownerName $ownerName --organizationName $orgNameOriginal --organizationId $organizationId + grace org set-name --output Verbose --ownerName $ownerName --organizationName $orgNameOriginal --newName $orgName + grace repo create --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoNameOriginal --repositoryId $repoId + grace repo set-name --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoNameOriginal --newName $repoName + grace branch create --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchId $branchId --branchName $branchName + + # 1..0 | ForEach-Object { + # $numberOfWords = Get-Random -Minimum 3 -Maximum 9 + # $start = Get-Random -Minimum 0 -Maximum ($words.Count - $numberOfWords) + # $message = '' + # for ($i = $0; $i -lt $numberOfWords; $i++) { + # $message += $words[$i + $start] + " " + # } + + # switch (Get-Random -Maximum 4) { + # 0 {grace branch save --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName} + # 1 {grace branch checkpoint --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName -m $message} + # 2 {grace branch commit --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName -m $message} + # 3 {grace branch tag --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName -m $message} + # } + # } + + grace branch delete --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --branchName $branchName + grace repo delete --output Verbose --ownerName $ownerName --organizationName $orgName --repositoryName $repoName --deleteReason "Test cleanup" + grace org delete --output Verbose --ownerName $ownerName --organizationName $orgName --deleteReason "Test cleanup" + grace owner delete --output Verbose --ownerName $ownerName --deleteReason "Test cleanup" } -ThrottleLimit 8 $endTime = Get-Date diff --git a/src/Grace.Actors/Branch.Actor.fs b/src/Grace.Actors/Branch.Actor.fs index d902627..e2c9241 100644 --- a/src/Grace.Actors/Branch.Actor.fs +++ b/src/Grace.Actors/Branch.Actor.fs @@ -192,7 +192,6 @@ module Branch = let isValid (command: BranchCommand) (metadata: EventMetadata) = task { let! branchEvents = this.BranchEvents - logToConsole (serialize branchEvents) if branchEvents.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) && (branchEvents.Count > 3) then return Error (GraceError.Create (BranchError.getErrorMessage DuplicateCorrelationId) metadata.CorrelationId) else @@ -226,7 +225,7 @@ module Branch = | Create (branchId, branchName, parentBranchId, basedOn, repositoryId, branchPermissions) -> return Created(branchId, branchName, parentBranchId, basedOn, repositoryId, branchPermissions) | Rebase referenceId -> return Rebased referenceId - | SetName organizationName -> return NameSet (organizationName) + | SetName branchName -> return NameSet branchName | BranchCommand.Promote (directoryId, sha256Hash, referenceText) -> let! referenceId = addReference directoryId sha256Hash referenceText ReferenceType.Promotion metadata.Properties.Add(nameof(ReferenceId), $"{referenceId}") diff --git a/src/Grace.Actors/Commands.Actor.fs b/src/Grace.Actors/Commands.Actor.fs index 2aea64c..74037f2 100644 --- a/src/Grace.Actors/Commands.Actor.fs +++ b/src/Grace.Actors/Commands.Actor.fs @@ -69,6 +69,7 @@ module Commands = | SetDefaultBranchName of defaultBranchName: BranchName | SetSaveDays of duration: double | SetCheckpointDays of duration: double + | SetName of repositoryName: RepositoryName | SetDescription of description: string | EnableSingleStepPromotion of enabled: bool | EnableComplexPromotion of enabled: bool diff --git a/src/Grace.Actors/Events.Actor.fs b/src/Grace.Actors/Events.Actor.fs index df4fd99..0c5d656 100644 --- a/src/Grace.Actors/Events.Actor.fs +++ b/src/Grace.Actors/Events.Actor.fs @@ -106,6 +106,7 @@ module Events = | CheckpointDaysSet of duration: double | EnabledSingleStepPromotion of enabled: bool | EnabledComplexPromotion of enabled: bool + | NameSet of repositoryName: RepositoryName | DescriptionSet of description: string | LogicalDeleted of force: bool * deleteReason: string | PhysicalDeleted diff --git a/src/Grace.Actors/Repository.Actor.fs b/src/Grace.Actors/Repository.Actor.fs index 0e48de7..905c642 100644 --- a/src/Grace.Actors/Repository.Actor.fs +++ b/src/Grace.Actors/Repository.Actor.fs @@ -124,6 +124,7 @@ module Repository = | CheckpointDaysSet days -> {currentRepositoryDto with CheckpointDays = days} | EnabledSingleStepPromotion enabled -> {currentRepositoryDto with EnabledSingleStepPromotion = enabled} | EnabledComplexPromotion enabled -> {currentRepositoryDto with EnabledComplexPromotion = enabled} + | NameSet repositoryName -> {currentRepositoryDto with RepositoryName = repositoryName} | DescriptionSet description -> {currentRepositoryDto with Description = description} | LogicalDeleted _ -> {currentRepositoryDto with DeletedAt = Some (getCurrentInstant())} | PhysicalDeleted -> currentRepositoryDto // Do nothing because it's about to be deleted anyway. @@ -395,6 +396,7 @@ module Repository = | SetCheckpointDays days -> return CheckpointDaysSet days | EnableSingleStepPromotion enabled -> return EnabledSingleStepPromotion enabled | EnableComplexPromotion enabled -> return EnabledComplexPromotion enabled + | SetName repositoryName -> return NameSet repositoryName | SetDescription description -> return DescriptionSet description | DeleteLogical (force, deleteReason) -> // Get the list of branches that aren't already deleted. diff --git a/src/Grace.Actors/Services.Actor.fs b/src/Grace.Actors/Services.Actor.fs index f87784f..5b323b0 100644 --- a/src/Grace.Actors/Services.Actor.fs +++ b/src/Grace.Actors/Services.Actor.fs @@ -174,11 +174,23 @@ module Services = return Uri("http://localhost:3500") } - /// Gets the OwnerId by returning OwnerId if provided, or searching by OwnerName. + /// Gets the OwnerId by checking for the existence of OwnerId if provided, or searching by OwnerName. let resolveOwnerId (ownerId: string) (ownerName: string) = task { - if not <| String.IsNullOrEmpty(ownerId) then - return Some ownerId + let mutable ownerGuid = Guid.Empty + if not <| String.IsNullOrEmpty(ownerId) && Guid.TryParse(ownerId, &ownerGuid) then + let mutable x = obj + let cached = memoryCache.TryGetValue(ownerGuid, &x) + if cached then + return Some ownerId + else + let actorProxy = actorProxyFactory.CreateActorProxy(ActorId(ownerId), ActorName.Owner) + let! exists = actorProxy.Exists() + if exists then + use newCacheEntry = memoryCache.CreateEntry(ownerGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) + return Some ownerId + else + return None elif String.IsNullOrEmpty(ownerName) then return None else @@ -200,18 +212,32 @@ module Services = return None else do! ownerNameActorProxy.SetOwnerId(ownerId) + ownerGuid <- Guid.Parse(ownerId) + use newCacheEntry = memoryCache.CreateEntry(ownerGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Some ownerId else return None | MongoDB -> return None } - + /// Gets the OrganizationId by either returning OrganizationId if provided, or searching by OrganizationName. let resolveOrganizationId (ownerId: string) (ownerName: string) (organizationId: string) (organizationName: string) = task { + let mutable organizationGuid = Guid.Empty match! resolveOwnerId ownerId ownerName with | Some ownerId -> - if not <| String.IsNullOrEmpty(organizationId) then - return Some organizationId + if not <| String.IsNullOrEmpty(organizationId) && Guid.TryParse(organizationId, &organizationGuid) then + let mutable x = obj + let cached = memoryCache.TryGetValue(organizationGuid, &x) + if cached then + return Some organizationId + else + let actorProxy = actorProxyFactory.CreateActorProxy(ActorId(organizationId), ActorName.Organization) + let! exists = actorProxy.Exists() + if exists then + use newCacheEntry = memoryCache.CreateEntry(organizationGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) + return Some organizationId + else + return None elif String.IsNullOrEmpty(organizationName) then return None else @@ -234,6 +260,8 @@ module Services = return None else do! organizationNameActorProxy.SetOrganizationId(organizationId) + organizationGuid <- Guid.Parse(organizationId) + use newCacheEntry = memoryCache.CreateEntry(organizationGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Some organizationId else return None | MongoDB -> return None @@ -243,12 +271,24 @@ module Services = /// Gets the RepositoryId by returning RepositoryId if provided, or searching by RepositoryName within the provided owner and organization. let resolveRepositoryId (ownerId: string) (ownerName: string) (organizationId: string) (organizationName: string) (repositoryId: string) (repositoryName: string) = task { + let mutable repositoryGuid = Guid.Empty match! resolveOwnerId ownerId ownerName with | Some ownerId -> match! resolveOrganizationId ownerId String.Empty organizationId organizationName with | Some organizationId -> - if not <| String.IsNullOrEmpty(repositoryId) then - return Some repositoryId + if not <| String.IsNullOrEmpty(repositoryId) && Guid.TryParse(repositoryId, &repositoryGuid) then + let mutable x = obj + let cached = memoryCache.TryGetValue(repositoryGuid, &x) + if cached then + return Some repositoryId + else + let actorProxy = actorProxyFactory.CreateActorProxy(ActorId(repositoryId), ActorName.Repository) + let! exists = actorProxy.Exists() + if exists then + use newCacheEntry = memoryCache.CreateEntry(repositoryGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) + return Some repositoryId + else + return None elif String.IsNullOrEmpty(repositoryName) then return None else @@ -264,7 +304,7 @@ module Services = .WithParameter("@organizationId", organizationId) .WithParameter("@ownerId", ownerId) .WithParameter("@class", "RepositoryDto") - let iterator = DefaultRetryPolicy.Execute(fun () -> cosmosContainer.GetItemQueryIterator(queryDefinition)) + let iterator = cosmosContainer.GetItemQueryIterator(queryDefinition) if iterator.HasMoreResults then let! currentResultSet = iterator.ReadNextAsync() let repositoryId = currentResultSet.FirstOrDefault({repositoryId = String.Empty}).repositoryId @@ -272,6 +312,8 @@ module Services = return None else do! repositoryNameActorProxy.SetRepositoryId(repositoryId) + repositoryGuid <- Guid.Parse(repositoryId) + use newCacheEntry = memoryCache.CreateEntry(repositoryGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Some repositoryId else return None | MongoDB -> return None @@ -328,8 +370,20 @@ module Services = /// Gets the BranchId by returning BranchId if provided, or searching by BranchName within the provided repository. let resolveBranchId repositoryId branchId branchName = task { + let mutable branchGuid = Guid.Empty if not <| String.IsNullOrEmpty(branchId) then - return Some branchId + let mutable x = obj + let cached = memoryCache.TryGetValue(branchGuid, &x) + if cached then + return Some branchId + else + let actorProxy = actorProxyFactory.CreateActorProxy(ActorId(branchId), ActorName.Branch) + let! exists = actorProxy.Exists() + if exists then + use newCacheEntry = memoryCache.CreateEntry(branchGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) + return Some branchId + else + return None elif String.IsNullOrEmpty(branchName) then return None else @@ -352,6 +406,8 @@ module Services = return None else do! branchNameActorProxy.SetBranchId(branchId) + branchGuid <- Guid.Parse(branchId) + use newCacheEntry = memoryCache.CreateEntry(branchGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Some branchId else return None | MongoDB -> return None @@ -435,6 +491,7 @@ module Services = return repositories } + /// Checks if the specified organization name is unique for the specified owner. let organizationNameIsUnique<'T> (ownerId: string) (ownerName: string) (organizationName: string) = task { match actorStateStorageProvider with @@ -466,6 +523,38 @@ module Services = | MongoDB -> return Ok false } + /// Checks if the specified repository name is unique for the specified organization. + let repositoryNameIsUnique<'T> (ownerId: string) (ownerName: string) (organizationId: string) (organizationName: string) (repositoryName: string) = + task { + match actorStateStorageProvider with + | Unknown -> return Ok false + | AzureCosmosDb -> + try + match! resolveOrganizationId ownerId ownerName organizationId organizationName with + | Some organizationId -> + let queryDefinition = QueryDefinition("""SELECT c["value"].RepositoryId FROM c WHERE c["value"].OrganizationId = @organizationId AND c["value"].RepositoryName = @repositoryName AND c["value"].Class = @class""") + .WithParameter("@organizationId", organizationId) + .WithParameter("@repositoryName", repositoryName) + .WithParameter("@class", "RepositoryDto") + //logToConsole (queryDefinition.QueryText.Replace("@organizationId", $"\"{organizationId}\"").Replace("@repositoryName", $"\"{repositoryName}\"")) + let iterator = cosmosContainer.GetItemQueryIterator(queryDefinition, requestOptions = queryRequestOptions) + if iterator.HasMoreResults then + let! currentResultSet = iterator.ReadNextAsync() + // If a row is returned, and repositoryId gets a value, then the repository name is not unique. + let repositoryId = currentResultSet.FirstOrDefault({repositoryId = String.Empty}).repositoryId + if String.IsNullOrEmpty(repositoryId) then + // The repository name is unique. + return Ok true + else + // The repository name is not unique. + return Ok false + else return Ok true // This else should never be hit. + | None -> return Ok false + with ex -> + return Error $"{createExceptionResponse ex}" + | MongoDB -> return Ok false + } + /// Gets a list of repositories for the specified organization. let getRepositories (organizationId: OrganizationId) (maxCount: int) includeDeleted = task { diff --git a/src/Grace.CLI/Properties/launchSettings.json b/src/Grace.CLI/Properties/launchSettings.json index be5d7ca..54925bb 100644 --- a/src/Grace.CLI/Properties/launchSettings.json +++ b/src/Grace.CLI/Properties/launchSettings.json @@ -93,10 +93,10 @@ "commandLineArgs": "owner get --ownerName Owner4", "workingDirectory": "D:\\Source\\GraceDemo\\Mika" }, - "grace org create /?": { + "grace org create": { "commandName": "Project", - "commandLineArgs": "org create /?", - "workingDirectory": "C:\\Temp" + "commandLineArgs": "org create --output Verbose --ownerName Owner9FEDA --organizationName Org9FEDc --organizationId 9871826b-73f4-41a4-bf4b-dea42ea95bf1", + "workingDirectory": "D:\\Source\\GraceDemo\\Ryan" } } } \ No newline at end of file diff --git a/src/Grace.SDK/Common.SDK.fs b/src/Grace.SDK/Common.SDK.fs index 51bdd12..f82df55 100644 --- a/src/Grace.SDK/Common.SDK.fs +++ b/src/Grace.SDK/Common.SDK.fs @@ -119,24 +119,21 @@ module Common = let serverUriWithRoute = Uri($"{Current().ServerUri}/{route}") //logToConsole $"serverUriWithRoute: {serverUriWithRoute}" let startTime = getCurrentInstant() - let! response = Constants.DefaultAsyncRetryPolicy.ExecuteAsync(fun _ -> httpClient.PostAsync(serverUriWithRoute, jsonContent parameters)) + let! response = httpClient.PostAsync(serverUriWithRoute, jsonContent parameters) let endTime = getCurrentInstant() if response.IsSuccessStatusCode then - //let! graceReturnValue = response.Content.ReadFromJsonAsync>(Constants.JsonSerializerOptions) - let! blah = response.Content.ReadAsStringAsync() - let graceReturnValue = JsonSerializer.Deserialize>(blah, Constants.JsonSerializerOptions) + let! graceReturnValue = response.Content.ReadFromJsonAsync>(Constants.JsonSerializerOptions) + //let! blah = response.Content.ReadAsStringAsync() + //let graceReturnValue = JsonSerializer.Deserialize>(blah, Constants.JsonSerializerOptions) return Ok graceReturnValue |> enhance ("ServerElapsedTime", $"{(endTime - startTime).TotalMilliseconds:F3} ms") else if response.StatusCode = HttpStatusCode.NotFound then return Error (GraceError.Create $"Server endpoint {route} not found." parameters.CorrelationId) else - use! responseStream = response.Content.ReadAsStreamAsync() - use memoryStream = new MemoryStream(int responseStream.Length) - do! responseStream.CopyToAsync(memoryStream) - use reader = new StreamReader(memoryStream, Encoding.UTF8) - let! s = reader.ReadToEndAsync() - let graceError = GraceError.Create s parameters.CorrelationId - //let! graceError = response.Content.ReadFromJsonAsync(Constants.JsonSerializerOptions) + //let! responseAsString = response.Content.ReadAsStringAsync() + //logToConsole $"responseAsString: {responseAsString}" + //let graceError = GraceError.Create responseAsString parameters.CorrelationId + let! graceError = response.Content.ReadFromJsonAsync(Constants.JsonSerializerOptions) return Error graceError |> enhance ("ServerElapsedTime", $"{(endTime - startTime).TotalMilliseconds:F3} ms") |> enhance ("StatusCode", $"{response.StatusCode}") with ex -> let exceptionResponse = Utilities.createExceptionResponse ex diff --git a/src/Grace.Server/Branch.Server.fs b/src/Grace.Server/Branch.Server.fs index 35b69e0..5b1d34a 100644 --- a/src/Grace.Server/Branch.Server.fs +++ b/src/Grace.Server/Branch.Server.fs @@ -19,6 +19,7 @@ open Grace.Shared.Validation.Common open Grace.Shared.Validation.Errors.Branch open Grace.Shared.Validation.Utilities open Microsoft.AspNetCore.Http +open Microsoft.Extensions.Logging open System open System.Collections.Concurrent open System.Collections.Generic @@ -33,6 +34,8 @@ module Branch = let activitySource = new ActivitySource("Branch") + let log = ApplicationContext.loggerFactory.CreateLogger("Branch.Server") + let actorProxyFactory = ApplicationContext.actorProxyFactory let getActorProxy (context: HttpContext) (branchId: string) = @@ -56,30 +59,51 @@ module Branch = let processCommand<'T when 'T :> BranchParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> Task) = task { try + let commandName = context.Items["Command"] :?> string + let graceIds = context.Items[nameof(GraceIds)] :?> GraceIds use activity = activitySource.StartActivity("processCommand", ActivityKind.Server) let! parameters = context |> parse<'T> + + let handleCommand branchId cmd = + task { + log.LogDebug("{currentInstant}: In Branch.Server.handleCommand: branchId: {branchId}.", getCurrentInstantExtended(), branchId) + let actorProxy = getActorProxy context branchId + + let! result = actorProxy.Handle cmd (Services.createMetadata context) + match result with + | Ok graceReturn -> + return! context |> result200Ok graceReturn + | Error graceError -> + log.LogDebug("{currentInstant}: In Branch.Server.handleCommand: error from actorProxy.Handle: {error}", getCurrentInstantExtended(), (graceError.ToString())) + return! context |> result400BadRequest {graceError with Properties = getPropertiesAsDictionary parameters} + } + let validationResults = Array.append (commonValidations parameters context) (validations parameters context) let! validationsPassed = validationResults |> allPass + log.LogDebug("{currentInstant}: In Branch.Server.processCommand: validationsPassed: {validationsPassed}.", getCurrentInstantExtended(), validationsPassed) + if validationsPassed then - let! repositoryId = resolveRepositoryId parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName - match repositoryId with - | Some repositoryId -> - parameters.RepositoryId <- repositoryId - match! resolveBranchId repositoryId parameters.BranchId parameters.BranchName with - | Some branchId -> - let actorProxy = getActorProxy context branchId - let! cmd = command parameters - match! actorProxy.Handle cmd (Services.createMetadata context) with - | Ok graceReturn -> return! context |> result200Ok graceReturn - | Error graceError -> return! context |> result400BadRequest graceError - | None -> - return! context |> result400BadRequest (GraceError.Create (BranchError.getErrorMessage BranchDoesNotExist) (getCorrelationId context)) - | None -> + let! cmd = command parameters + let! branchId = resolveBranchId graceIds.RepositoryId parameters.BranchId parameters.BranchName + match branchId, commandName = nameof(Create) with + | Some branchId, _ -> + // If Id is Some, then we know we have a valid Id. + if String.IsNullOrEmpty(parameters.BranchId) then parameters.BranchId <- branchId + return! handleCommand branchId cmd + | None, true -> + // If it's None, but this is a Create command, still valid, just use the Id from the parameters. + return! handleCommand parameters.BranchId cmd + | None, false -> + // If it's None, and this is not a Create command, then we have a bad request. + log.LogDebug("{currentInstant}: In Branch.Server.processCommand: resolveBranchId failed. Branch does not exist. repositoryId: {repositoryId}; repositoryName: {repositoryName}.", getCurrentInstantExtended(), parameters.RepositoryId, parameters.RepositoryName) return! context |> result400BadRequest (GraceError.Create (BranchError.getErrorMessage RepositoryDoesNotExist) (getCorrelationId context)) else let! error = validationResults |> getFirstError - let graceError = GraceError.Create (BranchError.getErrorMessage error) (getCorrelationId context) - graceError.Properties.Add("Path", context.Request.Path) + let errorMessage = BranchError.getErrorMessage error + log.LogDebug("{currentInstant}: error: {error}", getCurrentInstantExtended(), errorMessage) + let graceError = GraceError.CreateWithMetadata errorMessage (getCorrelationId context) (getPropertiesAsDictionary parameters) + graceError.Properties.Add("Path", context.Request.Path) + graceError.Properties.Add("Error", errorMessage) return! context |> result400BadRequest graceError with ex -> let graceError = GraceError.Create $"{Utilities.createExceptionResponse ex}" (getCorrelationId context) @@ -152,6 +176,7 @@ module Branch = parameters.InitialPermissions) } + context.Items.Add("Command", nameof(Create)) return! processCommand context validations command } @@ -169,8 +194,10 @@ module Branch = Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ParentBranchDoesNotExist Branch.branchAllowsReferenceType parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ReferenceType.Commit CommitIsDisabled |] - let command (parameters: RebaseParameters) = Rebase(parameters.BasedOn) |> returnTask + let command (parameters: RebaseParameters) = + Rebase(parameters.BasedOn) |> returnTask + context.Items.Add("Command", nameof(Rebase)) return! processCommand context validations command } @@ -189,8 +216,10 @@ module Branch = Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ParentBranchDoesNotExist Branch.branchAllowsReferenceType parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ReferenceType.Promotion PromotionIsDisabled |] - let command (parameters: CreateReferenceParameters) = BranchCommand.Promote(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + let command (parameters: CreateReferenceParameters) = + BranchCommand.Promote(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + context.Items.Add("Command", nameof(Promote)) return! processCommand context validations command } @@ -209,8 +238,10 @@ module Branch = Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist Branch.branchAllowsReferenceType parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ReferenceType.Commit CommitIsDisabled |] - let command (parameters: CreateReferenceParameters) = BranchCommand.Commit(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + let command (parameters: CreateReferenceParameters) = + BranchCommand.Commit(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + context.Items.Add("Command", nameof(Commit)) return! processCommand context validations command } @@ -229,8 +260,10 @@ module Branch = Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist Branch.branchAllowsReferenceType parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ReferenceType.Checkpoint CheckpointIsDisabled |] - let command (parameters: CreateReferenceParameters) = BranchCommand.Checkpoint(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + let command (parameters: CreateReferenceParameters) = + BranchCommand.Checkpoint(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + context.Items.Add("Command", nameof(Checkpoint)) return! processCommand context validations command } @@ -249,8 +282,10 @@ module Branch = Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist Branch.branchAllowsReferenceType parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ReferenceType.Save SaveIsDisabled |] - let command (parameters: CreateReferenceParameters) = BranchCommand.Save(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + let command (parameters: CreateReferenceParameters) = + BranchCommand.Save(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + context.Items.Add("Command", nameof(Save)) return! processCommand context validations command } @@ -269,8 +304,10 @@ module Branch = Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist Branch.branchAllowsReferenceType parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ReferenceType.Tag TagIsDisabled |] - let command (parameters: CreateReferenceParameters) = BranchCommand.Tag(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + let command (parameters: CreateReferenceParameters) = + BranchCommand.Tag(parameters.DirectoryId, parameters.Sha256Hash, ReferenceText parameters.Message) |> returnTask + context.Items.Add("Command", nameof(Tag)) return! processCommand context validations command } @@ -287,8 +324,10 @@ module Branch = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist |] - let command (parameters: EnableFeatureParameters) = EnablePromotion(parameters.Enabled) |> returnTask + let command (parameters: EnableFeatureParameters) = + EnablePromotion(parameters.Enabled) |> returnTask + context.Items.Add("Command", nameof(EnablePromotion)) return! processCommand context validations command } @@ -305,8 +344,10 @@ module Branch = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist |] - let command (parameters: EnableFeatureParameters) = EnableCommit(parameters.Enabled) |> returnTask + let command (parameters: EnableFeatureParameters) = + EnableCommit(parameters.Enabled) |> returnTask + context.Items.Add("Command", nameof(EnableCommit)) return! processCommand context validations command } @@ -323,8 +364,10 @@ module Branch = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist |] - let command (parameters: EnableFeatureParameters) = EnableCheckpoint(parameters.Enabled) |> returnTask + let command (parameters: EnableFeatureParameters) = + EnableCheckpoint(parameters.Enabled) |> returnTask + context.Items.Add("Command", nameof(EnableCheckpoint)) return! processCommand context validations command } @@ -341,8 +384,10 @@ module Branch = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName ParentBranchDoesNotExist |] - let command (parameters: EnableFeatureParameters) = EnableSave(parameters.Enabled) |> returnTask + let command (parameters: EnableFeatureParameters) = + EnableSave(parameters.Enabled) |> returnTask + context.Items.Add("Command", nameof(EnableSave)) return! processCommand context validations command } @@ -359,8 +404,10 @@ module Branch = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist |] - let command (parameters: EnableFeatureParameters) = EnableTag(parameters.Enabled) |> returnTask + let command (parameters: EnableFeatureParameters) = + EnableTag(parameters.Enabled) |> returnTask + context.Items.Add("Command", nameof(EnableTag)) return! processCommand context validations command } @@ -377,8 +424,10 @@ module Branch = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Branch.branchExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName parameters.BranchId parameters.BranchName BranchDoesNotExist |] - let command (parameters: DeleteBranchParameters) = DeleteLogical (parameters.Force, parameters.DeleteReason) |> returnTask + let command (parameters: DeleteBranchParameters) = + DeleteLogical (parameters.Force, parameters.DeleteReason) |> returnTask + context.Items.Add("Command", nameof(DeleteLogical)) return! processCommand context validations command } diff --git a/src/Grace.Server/Middleware/ValidateIds.Middleware.fs b/src/Grace.Server/Middleware/ValidateIds.Middleware.fs index 1d11afd..1eb9459 100644 --- a/src/Grace.Server/Middleware/ValidateIds.Middleware.fs +++ b/src/Grace.Server/Middleware/ValidateIds.Middleware.fs @@ -177,12 +177,7 @@ type ValidateIdsMiddleware(next: RequestDelegate) = // Resolve the OwnerId based on the provided Id and Name. match! resolveOwnerId ownerId ownerName with | Some resolvedOwnerId -> - // Check to see if the Owner exists. - match! Owner.ownerExists resolvedOwnerId ownerName Owner.OwnerError.OwnerDoesNotExist with - | Ok _ -> - graceIds <- {graceIds with OwnerId = resolvedOwnerId; HasOwner = true} - | Error error -> - notFound <- true + graceIds <- {graceIds with OwnerId = resolvedOwnerId; HasOwner = true} | None -> badRequest <- true @@ -199,12 +194,7 @@ type ValidateIdsMiddleware(next: RequestDelegate) = // Resolve the OrganizationId based on the provided Id and Name. match! resolveOrganizationId ownerId ownerName organizationId organizationName with | Some resolvedOrganizationId -> - // Check to see if the Organization exists. - match! Organization.organizationExists ownerId ownerName resolvedOrganizationId organizationName Organization.OrganizationError.OrganizationDoesNotExist with - | Ok _ -> - graceIds <- {graceIds with OrganizationId = resolvedOrganizationId; HasOrganization = true} - | Error error -> - notFound <- true + graceIds <- {graceIds with OrganizationId = resolvedOrganizationId; HasOrganization = true} | None -> badRequest <- true @@ -221,12 +211,7 @@ type ValidateIdsMiddleware(next: RequestDelegate) = // Resolve the RepositoryId based on the provided Id and Name. match! resolveRepositoryId ownerId ownerName organizationId organizationName repositoryId repositoryName with | Some resolvedRepositoryId -> - // Check to see if the Repository exists. - match! Repository.repositoryExists ownerId ownerName organizationId organizationName resolvedRepositoryId repositoryName Repository.RepositoryError.RepositoryDoesNotExist with - | Ok _ -> - graceIds <- {graceIds with RepositoryId = resolvedRepositoryId; HasRepository = true} - | Error error -> - notFound <- true + graceIds <- {graceIds with RepositoryId = resolvedRepositoryId; HasRepository = true} | None -> badRequest <- true diff --git a/src/Grace.Server/Organization.Server.fs b/src/Grace.Server/Organization.Server.fs index d8c139a..d3718ac 100644 --- a/src/Grace.Server/Organization.Server.fs +++ b/src/Grace.Server/Organization.Server.fs @@ -17,6 +17,7 @@ open Grace.Shared.Validation.Errors.Organization open Grace.Shared.Validation.Utilities open Grace.Shared.Types open Microsoft.AspNetCore.Http +open Microsoft.Extensions.Logging open System open System.Threading.Tasks open System.Diagnostics @@ -28,6 +29,8 @@ module Organization = let activitySource = new ActivitySource("Organization") + let log = ApplicationContext.loggerFactory.CreateLogger("Organization.Server") + let actorProxyFactory = ApplicationContext.actorProxyFactory let getActorProxy (context: HttpContext) (organizationId: string) = @@ -37,32 +40,54 @@ module Organization = let processCommand<'T when 'T :> OrganizationParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> Task) = task { try + let commandName = context.Items["Command"] :?> string use activity = activitySource.StartActivity("processCommand", ActivityKind.Server) let! parameters = context |> parse<'T> - let validationResults = validations parameters context - let! validationsPassed = validationResults |> allPass - if validationsPassed then - let! organizationId = resolveOrganizationId parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName - match organizationId with - | Some organizationId -> + + let handleCommand organizationId cmd = + task { + log.LogDebug("{currentInstant}: In Organization.Server.handleCommand: organizationId: {organizationId}.", getCurrentInstantExtended(), organizationId) let actorProxy = getActorProxy context organizationId - let! cmd = command parameters + let! result = actorProxy.Handle cmd (Services.createMetadata context) match result with - | Ok graceReturn -> - return! context |> result200Ok graceReturn - | Error graceError -> + | Ok graceReturn -> + return! context |> result200Ok graceReturn + | Error graceError -> + log.LogDebug("{currentInstant}: In Organization.Server.handleCommand: error from actorProxy.Handle: {error}", getCurrentInstantExtended(), (graceError.ToString())) return! context |> result400BadRequest {graceError with Properties = getPropertiesAsDictionary parameters} - | None -> + } + + let validationResults = validations parameters context + let! validationsPassed = validationResults |> allPass + log.LogDebug("{currentInstant}: In Organization.Server.processCommand: validationsPassed: {validationsPassed}.", getCurrentInstantExtended(), validationsPassed) + + if validationsPassed then + let! cmd = command parameters + let! organizationId = resolveOrganizationId parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName + match organizationId, commandName = nameof(Create) with + | Some organizationId, _ -> + // If Id is Some, then we know we have a valid Id. + if String.IsNullOrEmpty(parameters.OrganizationId) then parameters.OrganizationId<- organizationId + return! handleCommand organizationId cmd + | None, true -> + // If it's None, but this is a Create command, still valid, just use the Id from the parameters. + return! handleCommand parameters.OrganizationId cmd + | None, false -> + // If it's None, and this is not a Create command, then we have a bad request. + log.LogDebug("{currentInstant}: In Organization.Server.processCommand: resolveOrganizationId failed. Organization does not exist. organizationId: {organizationId}; organizationName: {organizationName}.", getCurrentInstantExtended(), parameters.OrganizationId, parameters.OrganizationName) return! context |> result400BadRequest (GraceError.CreateWithMetadata (OrganizationError.getErrorMessage OrganizationDoesNotExist) (getCorrelationId context) (getPropertiesAsDictionary parameters)) else let! error = validationResults |> getFirstError - let graceError = GraceError.CreateWithMetadata (OrganizationError.getErrorMessage error) (getCorrelationId context) (getPropertiesAsDictionary parameters) + let errorMessage = OrganizationError.getErrorMessage error + log.LogDebug("{currentInstant}: error: {error}", getCurrentInstantExtended(), errorMessage) + let graceError = GraceError.CreateWithMetadata errorMessage (getCorrelationId context) (getPropertiesAsDictionary parameters) graceError.Properties.Add("Path", context.Request.Path) - graceError.Properties.Add("Error", OrganizationError.getErrorMessage error) + graceError.Properties.Add("Error", errorMessage) return! context |> result400BadRequest graceError with ex -> - return! context |> result500ServerError (GraceError.Create $"{Utilities.createExceptionResponse ex}" (getCorrelationId context)) + log.LogError(ex, "{currentInstant}: Exception in Organization.Server.processCommand. CorrelationId: {correlationId}.", getCurrentInstantExtended(), (getCorrelationId context)) + return! context |> result500ServerError (GraceError.Create $"{createExceptionResponse ex}" (getCorrelationId context)) } let processQuery<'T, 'U when 'T :> OrganizationParameters> (context: HttpContext) (parameters: 'T) (validations: Validations<'T>) (maxCount: int) (query: QueryResult) = @@ -85,7 +110,7 @@ module Organization = graceError.Properties.Add("Path", context.Request.Path) return! context |> result400BadRequest graceError with ex -> - return! context |> result500ServerError (GraceError.Create $"{Utilities.createExceptionResponse ex}" (getCorrelationId context)) + return! context |> result500ServerError (GraceError.Create $"{createExceptionResponse ex}" (getCorrelationId context)) } /// Create an organization. @@ -109,9 +134,11 @@ module Organization = let! ownerId = resolveOwnerId parameters.OwnerId parameters.OwnerName let ownerIdGuid = Guid.Parse(ownerId.Value) let organizationIdGuid = Guid.Parse(parameters.OrganizationId) - return OrganizationCommand.Create (organizationIdGuid, OrganizationName parameters.OrganizationName, ownerIdGuid) + return Create (organizationIdGuid, OrganizationName parameters.OrganizationName, ownerIdGuid) } - + + log.LogDebug("{currentInstant}: In Grace.Server.Create.", getCurrentInstantExtended()) + context.Items.Add("Command", nameof(Create)) return! processCommand context validations command } @@ -132,8 +159,9 @@ module Organization = Organization.organizationIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationIsDeleted |] let command (parameters: SetOrganizationNameParameters) = - OrganizationCommand.SetName (OrganizationName parameters.NewName) |> returnTask + SetName (OrganizationName parameters.NewName) |> returnTask + context.Items.Add("Command", nameof(SetName)) return! processCommand context validations command } @@ -152,8 +180,10 @@ module Organization = Organization.organizationExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationDoesNotExist Organization.organizationIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationIsDeleted |] - let command (parameters: SetOrganizationTypeParameters) = OrganizationCommand.SetType (Utilities.discriminatedUnionFromString(parameters.OrganizationType).Value) |> returnTask + let command (parameters: SetOrganizationTypeParameters) = + SetType (discriminatedUnionFromString(parameters.OrganizationType).Value) |> returnTask + context.Items.Add("Command", nameof(SetType)) return! processCommand context validations command } @@ -174,8 +204,9 @@ module Organization = Organization.organizationIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationIsDeleted |] let command (parameters: SetOrganizationSearchVisibilityParameters) = - OrganizationCommand.SetSearchVisibility (Utilities.discriminatedUnionFromString(parameters.SearchVisibility).Value) |> returnTask + SetSearchVisibility (discriminatedUnionFromString(parameters.SearchVisibility).Value) |> returnTask + context.Items.Add("Command", nameof(SetSearchVisibility)) return! processCommand context validations command } @@ -194,8 +225,10 @@ module Organization = Organization.organizationExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationDoesNotExist Organization.organizationIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationIsDeleted |] - let command (parameters: SetOrganizationDescriptionParameters) = OrganizationCommand.SetDescription(parameters.Description) |> returnTask + let command (parameters: SetOrganizationDescriptionParameters) = + SetDescription(parameters.Description) |> returnTask + context.Items.Add("Command", nameof(SetDescription)) return! processCommand context validations command } @@ -223,7 +256,7 @@ module Organization = let! parameters = context |> parse return! processQuery context parameters validations 1 query with ex -> - return! context |> result500ServerError (GraceError.Create $"{Utilities.createExceptionResponse ex}" (getCorrelationId context)) + return! context |> result500ServerError (GraceError.Create $"{createExceptionResponse ex}" (getCorrelationId context)) } /// Delete an organization. @@ -241,8 +274,10 @@ module Organization = Organization.organizationExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationDoesNotExist Organization.organizationIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationIsDeleted |] - let command (parameters: DeleteOrganizationParameters) = OrganizationCommand.DeleteLogical (parameters.Force, parameters.DeleteReason) |> returnTask + let command (parameters: DeleteOrganizationParameters) = + DeleteLogical (parameters.Force, parameters.DeleteReason) |> returnTask + context.Items.Add("Command", nameof(DeleteLogical)) return! processCommand context validations command } @@ -261,8 +296,10 @@ module Organization = Organization.organizationExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationDoesNotExist Organization.organizationIsDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationIsNotDeleted |] - let command (parameters: OrganizationParameters) = OrganizationCommand.Undelete |> returnTask + let command (parameters: OrganizationParameters) = + Undelete |> returnTask + context.Items.Add("Command", nameof(Undelete)) return! processCommand context validations command } @@ -290,5 +327,5 @@ module Organization = let! parameters = context |> parse return! processQuery context parameters validations 1 query with ex -> - return! context |> result500ServerError (GraceError.Create $"{Utilities.createExceptionResponse ex}" (getCorrelationId context)) + return! context |> result500ServerError (GraceError.Create $"{createExceptionResponse ex}" (getCorrelationId context)) } diff --git a/src/Grace.Server/Owner.Server.fs b/src/Grace.Server/Owner.Server.fs index e6f0660..40ecb4b 100644 --- a/src/Grace.Server/Owner.Server.fs +++ b/src/Grace.Server/Owner.Server.fs @@ -11,6 +11,7 @@ open Grace.Server.ApplicationContext open Grace.Server.Services open Grace.Server.Validations open Grace.Shared +open Grace.Shared.Constants open Grace.Shared.Parameters.Owner open Grace.Shared.Validation.Common open Grace.Shared.Validation.Utilities @@ -25,9 +26,6 @@ open System open System.Diagnostics open System.Linq open System.Threading.Tasks -open Grace.Shared.Utilities -open System.Reflection -open Grace.Shared.Constants module Owner = @@ -47,35 +45,54 @@ module Owner = let processCommand<'T when 'T :> OwnerParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> Task) = task { try + let commandName = context.Items["Command"] :?> string use activity = activitySource.StartActivity("processCommand", ActivityKind.Server) let! parameters = context |> parse<'T> - let! cmd = command parameters - let commandName = discriminatedUnionFullName cmd + + let handleCommand ownerId cmd = + task { + log.LogDebug("{currentInstant}: In Owner.Server.handleCommand: ownerId: {ownerId}.", getCurrentInstantExtended(), ownerId) + let actorProxy = getActorProxy context ownerId + + let! result = actorProxy.Handle cmd (Services.createMetadata context) + match result with + | Ok graceReturn -> + return! context |> result200Ok graceReturn + | Error graceError -> + log.LogDebug("{currentInstant}: In Owner.Server.handleCommand: error from actorProxy.Handle: {error}", getCurrentInstantExtended(), (graceError.ToString())) + return! context |> result400BadRequest {graceError with Properties = getPropertiesAsDictionary parameters} + } let validationResults = validations parameters context let! validationsPassed = validationResults |> allPass + log.LogDebug("{currentInstant}: In Owner.Server.processCommand: validationsPassed: {validationsPassed}.", getCurrentInstantExtended(), validationsPassed) + if validationsPassed then + let! cmd = command parameters let! ownerId = resolveOwnerId parameters.OwnerId parameters.OwnerName - match ownerId with - | Some ownerId -> - let actorProxy = getActorProxy context ownerId - match! actorProxy.Handle cmd (Services.createMetadata context) with - | Ok graceReturn -> - log.Log(LogLevel.Information, "{currentInstant}: OwnerId: {ownerId}; command {commandName} succeeded.", getCurrentInstantExtended(), ownerId, commandName) - return! context |> result200Ok graceReturn - | Error graceError -> - log.Log(LogLevel.Information, "{currentInstant}: OwnerId: {ownerId}; command {commandName} had an error {graceError}.", getCurrentInstantExtended(), ownerId, commandName, graceError) - return! context |> result400BadRequest {graceError with Properties = (getPropertiesAsDictionary parameters)} - | None -> + + match ownerId, commandName = nameof(Create) with + | Some ownerId, _ -> + // If ownerId is Some, then we have a valid ownerId. + if String.IsNullOrEmpty(parameters.OwnerId) then parameters.OwnerId<- ownerId + return! handleCommand ownerId cmd + | None, true -> + // If it's None, but this is a Create command, still valid, just use the ownerId from the parameters. + return! handleCommand parameters.OwnerId cmd + | None, false -> + // If it's None, and this is not a Create command, then we have a bad request. log.Log(LogLevel.Information, "{currentInstant}: Error: OwnerNotFound; OwnerId: {ownerId}; OwnerName: {ownerName}; command {commandName}.", getCurrentInstantExtended(), parameters.OwnerId, parameters.OwnerName, commandName) return! context |> result400BadRequest (GraceError.CreateWithMetadata (OwnerError.getErrorMessage OwnerDoesNotExist) (getCorrelationId context) (getPropertiesAsDictionary parameters)) else let! error = validationResults |> getFirstError - let graceError = GraceError.CreateWithMetadata (OwnerError.getErrorMessage error) (getCorrelationId context) (getPropertiesAsDictionary parameters) + let errorMessage = OwnerError.getErrorMessage error + log.LogDebug("{currentInstant}: error: {error}", getCurrentInstantExtended(), errorMessage) + let graceError = GraceError.CreateWithMetadata errorMessage (getCorrelationId context) (getPropertiesAsDictionary parameters) graceError.Properties.Add("Path", context.Request.Path) - log.Log(LogLevel.Information, "{currentInstant}: Validation failed; {graceError}.", getCurrentInstantExtended(), commandName, graceError) + graceError.Properties.Add("Error", errorMessage) return! context |> result400BadRequest graceError with ex -> + log.LogError(ex, "{currentInstant}: Exception in Organization.Server.processCommand. CorrelationId: {correlationId}.", getCurrentInstantExtended(), (getCorrelationId context)) return! context |> result500ServerError (GraceError.Create $"{Utilities.createExceptionResponse ex}" (getCorrelationId context)) } @@ -117,8 +134,9 @@ module Owner = let command (parameters: CreateOwnerParameters) = let ownerIdGuid = Guid.Parse(parameters.OwnerId) - OwnerCommand.Create (ownerIdGuid, OwnerName parameters.OwnerName) |> returnTask + Create (ownerIdGuid, OwnerName parameters.OwnerName) |> returnTask + context.Items.Add("Command", nameof(Create)) return! processCommand context validations command } @@ -136,10 +154,10 @@ module Owner = Owner.ownerIsNotDeleted parameters.OwnerId parameters.OwnerName OwnerIsDeleted Owner.ownerNameDoesNotExist parameters.NewName OwnerNameAlreadyExists |] - let ownerCommand = OwnerCommand.SetName let command (parameters: SetOwnerNameParameters) = - ownerCommand (OwnerName parameters.NewName) |> returnTask + SetName (OwnerName parameters.NewName) |> returnTask + context.Items.Add("Command", nameof(SetName)) return! processCommand context validations command } @@ -158,6 +176,7 @@ module Owner = let command (parameters: SetOwnerTypeParameters) = OwnerCommand.SetType (Utilities.discriminatedUnionFromString(parameters.OwnerType).Value) |> returnTask + context.Items.Add("Command", nameof(SetType)) return! processCommand context validations command } @@ -175,6 +194,7 @@ module Owner = let command (parameters: SetOwnerSearchVisibilityParameters) = OwnerCommand.SetSearchVisibility (Utilities.discriminatedUnionFromString(parameters.SearchVisibility).Value) |> returnTask + context.Items.Add("Command", nameof(SetSearchVisibility)) return! processCommand context validations command } @@ -192,6 +212,7 @@ module Owner = let command (parameters: SetOwnerDescriptionParameters) = OwnerCommand.SetDescription (parameters.Description) |> returnTask + context.Items.Add("Command", nameof(SetDescription)) return! processCommand context validations command } @@ -229,6 +250,7 @@ module Owner = let command (parameters: DeleteOwnerParameters) = OwnerCommand.DeleteLogical (parameters.Force, parameters.DeleteReason) |> returnTask + context.Items.Add("Command", nameof(Delete)) return! processCommand context validations command } @@ -245,6 +267,7 @@ module Owner = let command (parameters: OwnerParameters) = OwnerCommand.Undelete |> returnTask + context.Items.Add("Command", nameof(Undelete)) return! processCommand context validations command } diff --git a/src/Grace.Server/Repository.Server.fs b/src/Grace.Server/Repository.Server.fs index 282b739..b46cb7b 100644 --- a/src/Grace.Server/Repository.Server.fs +++ b/src/Grace.Server/Repository.Server.fs @@ -22,6 +22,7 @@ open Grace.Shared.Validation open Grace.Shared.Validation.Utilities open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Mvc +open Microsoft.Extensions.Logging open NodaTime open OpenTelemetry.Trace open System @@ -34,7 +35,6 @@ open System.Text open System.Text.Json open Repository open FSharpPlus.Data -open Giraffe.ViewEngine.HtmlElements module Repository = @@ -42,6 +42,8 @@ module Repository = let activitySource = new ActivitySource("Repository") + let log = ApplicationContext.loggerFactory.CreateLogger("Repository.Server") + let actorProxyFactory = ApplicationContext.actorProxyFactory let getActorProxy (context: HttpContext) (repositoryId: string) = @@ -62,31 +64,53 @@ module Repository = let processCommand<'T when 'T :> RepositoryParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> Task) = task { try + let commandName = context.Items["Command"] :?> string use activity = activitySource.StartActivity("processCommand", ActivityKind.Server) let! parameters = context |> parse<'T> + + let handleCommand repositoryId cmd = + task { + log.LogDebug("{currentInstant}: In Repository.Server.handleCommand: repositoryId: {repositoryId}.", getCurrentInstantExtended(), repositoryId) + let actorProxy = getActorProxy context repositoryId + + let! result = actorProxy.Handle cmd (Services.createMetadata context) + match result with + | Ok graceReturn -> + return! context |> result200Ok graceReturn + | Error graceError -> + log.LogDebug("{currentInstant}: In Repository.Server.handleCommand: error from actorProxy.Handle: {error}", getCurrentInstantExtended(), (graceError.ToString())) + return! context |> result400BadRequest {graceError with Properties = getPropertiesAsDictionary parameters} + } + let validationResults = Array.append (commonValidations parameters context) (validations parameters context) let! validationsPassed = validationResults |> allPass + log.LogDebug("{currentInstant}: In Repository.Server.processCommand: validationsPassed: {validationsPassed}.", getCurrentInstantExtended(), validationsPassed) + if validationsPassed then + let! cmd = command parameters let! repositoryId = resolveRepositoryId parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName - match repositoryId with - | Some repositoryId -> + match repositoryId, commandName = nameof(Create) with + | Some repositoryId, _ -> + // If Id is Some, then we know we have a valid Id. if String.IsNullOrEmpty(parameters.RepositoryId) then parameters.RepositoryId <- repositoryId - let actorProxy = getActorProxy context repositoryId - let! cmd = command parameters - let! result = actorProxy.Handle cmd (Services.createMetadata context) - match result with - | Ok graceReturn -> - return! context |> result200Ok graceReturn - | Error graceError -> - return! context |> result400BadRequest {graceError with Properties = getPropertiesAsDictionary parameters} - | None -> + return! handleCommand repositoryId cmd + | None, true -> + // If it's None, but this is a Create command, still valid, just use the Id from the parameters. + return! handleCommand parameters.RepositoryId cmd + | None, false -> + // If it's None, and this is not a Create command, then we have a bad request. + log.LogDebug("{currentInstant}: In Repository.Server.processCommand: resolveRepositoryId failed. Repository does not exist. repositoryId: {repositoryId}; repositoryName: {repositoryName}.", getCurrentInstantExtended(), parameters.RepositoryId, parameters.RepositoryName) return! context |> result400BadRequest (GraceError.CreateWithMetadata (RepositoryError.getErrorMessage RepositoryDoesNotExist) (getCorrelationId context) (getPropertiesAsDictionary parameters)) else let! error = validationResults |> getFirstError - let graceError = GraceError.CreateWithMetadata (RepositoryError.getErrorMessage error) (getCorrelationId context) (getPropertiesAsDictionary parameters) + let errorMessage = RepositoryError.getErrorMessage error + log.LogDebug("{currentInstant}: error: {error}", getCurrentInstantExtended(), errorMessage) + let graceError = GraceError.CreateWithMetadata errorMessage (getCorrelationId context) (getPropertiesAsDictionary parameters) graceError.Properties.Add("Path", context.Request.Path) + graceError.Properties.Add("Error", errorMessage) return! context |> result400BadRequest graceError with ex -> + log.LogError(ex, "{currentInstant}: Exception in Repository.Server.processCommand. CorrelationId: {correlationId}.", getCurrentInstantExtended(), (getCorrelationId context)) return! context |> result500ServerError (GraceError.Create $"{createExceptionResponse ex}" (getCorrelationId context)) } @@ -133,7 +157,9 @@ module Repository = String.isNotEmpty parameters.RepositoryName RepositoryNameIsRequired String.isValidGraceName parameters.RepositoryName InvalidRepositoryName Owner.ownerExists parameters.OwnerId parameters.OwnerName OwnerDoesNotExist - Organization.organizationExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationDoesNotExist |] + Organization.organizationExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationDoesNotExist + Repository.repositoryIdDoesNotExist parameters.RepositoryId RepositoryIdAlreadyExists + Repository.repositoryNameIsUnique parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryName RepositoryNameAlreadyExists |] let command (parameters: CreateRepositoryParameters) = task { @@ -143,6 +169,7 @@ module Repository = (Guid.Parse(ownerId.Value)), (Guid.Parse(organizationId.Value))) } + context.Items.Add("Command", nameof(Create)) return! processCommand context validations command } @@ -159,6 +186,7 @@ module Repository = let command (parameters: SetRepositoryVisibilityParameters) = SetVisibility(discriminatedUnionFromString(parameters.Visibility).Value) |> returnTask + context.Items.Add("Command", nameof(SetVisibility)) return! processCommand context validations command } @@ -172,8 +200,10 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted |] - let command (parameters: SetSaveDaysParameters) = SetSaveDays(parameters.SaveDays) |> returnTask + let command (parameters: SetSaveDaysParameters) = + SetSaveDays(parameters.SaveDays) |> returnTask + context.Items.Add("Command", nameof(SetSaveDays)) return! processCommand context validations command } @@ -189,6 +219,7 @@ module Repository = let command (parameters: SetCheckpointDaysParameters) = SetCheckpointDays(parameters.CheckpointDays) |> returnTask + context.Items.Add("Command", nameof(SetCheckpointDays)) return! processCommand context validations command } @@ -201,8 +232,10 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted |] - let command (parameters: EnablePromotionTypeParameters) = EnableSingleStepPromotion (parameters.Enabled) |> returnTask + let command (parameters: EnablePromotionTypeParameters) = + EnableSingleStepPromotion (parameters.Enabled) |> returnTask + context.Items.Add("Command", nameof(EnableSingleStepPromotion)) return! processCommand context validations command } @@ -215,8 +248,10 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted |] - let command (parameters: EnablePromotionTypeParameters) = EnableComplexPromotion (parameters.Enabled) |> returnTask + let command (parameters: EnablePromotionTypeParameters) = + EnableComplexPromotion (parameters.Enabled) |> returnTask + context.Items.Add("Command", nameof(EnableComplexPromotion)) return! processCommand context validations command } @@ -233,6 +268,7 @@ module Repository = let command (parameters: SetRepositoryStatusParameters) = SetRepositoryStatus(discriminatedUnionFromString(parameters.Status).Value) |> returnTask + context.Items.Add("Command", nameof(SetRepositoryStatus)) return! processCommand context validations command } @@ -246,8 +282,10 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted |] - let command (parameters: SetDefaultServerApiVersionParameters) = SetDefaultServerApiVersion(parameters.DefaultServerApiVersion) |> returnTask + let command (parameters: SetDefaultServerApiVersionParameters) = + SetDefaultServerApiVersion(parameters.DefaultServerApiVersion) |> returnTask + context.Items.Add("Command", nameof(SetDefaultServerApiVersion)) return! processCommand context validations command } @@ -260,8 +298,10 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted |] - let command (parameters: RecordSavesParameters) = SetRecordSaves (parameters.RecordSaves) |> returnTask + let command (parameters: RecordSavesParameters) = + SetRecordSaves (parameters.RecordSaves) |> returnTask + context.Items.Add("Command", nameof(SetRecordSaves)) return! processCommand context validations command } @@ -275,8 +315,29 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted |] - let command (parameters: SetRepositoryDescriptionParameters) = SetDescription (parameters.Description) |> returnTask + let command (parameters: SetRepositoryDescriptionParameters) = + SetDescription (parameters.Description) |> returnTask + + context.Items.Add("Command", nameof(SetDescription)) + return! processCommand context validations command + } + + /// Sets the name of the repository. + let SetName: HttpHandler = + fun (next: HttpFunc) (context: HttpContext) -> + task { + let validations (parameters: SetRepositoryNameParameters) (context: HttpContext) = + [| Input.eitherIdOrNameMustBeProvided parameters.RepositoryId parameters.RepositoryName EitherRepositoryIdOrRepositoryNameRequired + String.isNotEmpty parameters.NewName RepositoryNameIsRequired + String.isValidGraceName parameters.NewName InvalidRepositoryName + Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist + Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted + Repository.repositoryNameIsUnique parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.NewName RepositoryNameAlreadyExists |] + + let command (parameters: SetRepositoryNameParameters) = + SetName (parameters.NewName) |> returnTask + context.Items.Add("Command", nameof(SetName)) return! processCommand context validations command } @@ -290,8 +351,10 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsDeleted |] - let command (parameters: DeleteRepositoryParameters) = DeleteLogical (parameters.Force, parameters.DeleteReason) |> returnTask + let command (parameters: DeleteRepositoryParameters) = + DeleteLogical (parameters.Force, parameters.DeleteReason) |> returnTask + context.Items.Add("Command", nameof(DeleteLogical)) return! processCommand context validations command } @@ -304,8 +367,10 @@ module Repository = Repository.repositoryExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryDoesNotExist Repository.repositoryIsDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName RepositoryIsNotDeleted |] - let command (parameters: RepositoryParameters) = Undelete |> returnTask + let command (parameters: RepositoryParameters) = + Undelete |> returnTask + context.Items.Add("Command", nameof(Undelete)) return! processCommand context validations command } diff --git a/src/Grace.Server/Services.Server.fs b/src/Grace.Server/Services.Server.fs index 000de6e..18ab020 100644 --- a/src/Grace.Server/Services.Server.fs +++ b/src/Grace.Server/Services.Server.fs @@ -3,28 +3,19 @@ open Dapr.Actors open Giraffe open Grace.Actors -open Grace.Actors.BranchName -open Grace.Actors.Organization -open Grace.Actors.OrganizationName -open Grace.Actors.Owner -open Grace.Actors.OwnerName -open Grace.Actors.Repository -open Grace.Actors.RepositoryName open Grace.Actors.Constants +open Grace.Actors.Interfaces +open Grace.Actors.Services open Grace.Server.ApplicationContext -open Grace.Actors.Reference open Grace.Shared open Grace.Shared.Constants -open Grace.Shared.Dto.Branch -open Grace.Shared.Dto.Reference -open Grace.Shared.Dto.Repository open Grace.Shared.Parameters.Common +open Grace.Shared.Resources.Text open Grace.Shared.Types open Grace.Shared.Utilities -open Grace.Shared.Validation.Utilities open Microsoft.AspNetCore.Http -open Microsoft.Azure.Cosmos -open Microsoft.Azure.Cosmos.Linq +open Microsoft.Extensions.Caching.Memory +open Microsoft.Extensions.Logging open NodaTime open System open System.Collections.Concurrent @@ -38,6 +29,8 @@ open System.Text.Json module Services = + let log = ApplicationContext.loggerFactory.CreateLogger("Server.Services") + /// Defines the type of all server queries in Grace. /// /// Takes an HttpContext, the MaxCount of results to return, and the ActorProxy to use for the query, and returns a Task containing the return value. @@ -92,6 +85,8 @@ module Services = .AddTag("http.status_code", statusCode) |> ignore context.SetStatusCode(statusCode) + log.LogDebug("{currentInstant}: In returnResult: StatusCode: {statusCode}; result: {result}", getCurrentInstantExtended(), statusCode, serialize result) + // .WriteJsonAsync() uses Grace's Constants.JsonSerializerOptions through DI. return! context.WriteJsonAsync(result) with ex -> @@ -119,3 +114,29 @@ module Services = /// Adds common attributes to the current OpenTelemetry activity, and returns the result with a 500 Internal server error status. let result500ServerError<'T> = returnResult<'T> StatusCodes.Status500InternalServerError + + /// Validates that the owner exists in the database. + //let confirmOwnerId<'T> context ownerId ownerName = + // task { + // let mutable ownerGuid = Guid.Empty + + // match! resolveOwnerId ownerId ownerName with + // | Some ownerId -> + // if Guid.TryParse(ownerId, &ownerGuid) then + // let mutable x = null + // let cached = memoryCache.TryGetValue(ownerGuid, &x) + // if cached then + // return Ok ownerGuid + // else + // let actorId = Owner.GetActorId(ownerGuid) + // let ownerActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Owner) + // let! exists = ownerActorProxy.Exists() + // if exists then + // use newCacheEntry = memoryCache.CreateEntry(ownerGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) + // return Ok ownerGuid + // else + // return Error (GraceError.Create (getLocalizedString OwnerDoesNotExist) (getCorrelationId context)) + // else + // return Ok ownerGuid + // | None -> return Error (GraceError.Create (getLocalizedString OwnerDoesNotExist) (getCorrelationId context)) + // } diff --git a/src/Grace.Server/Startup.Server.fs b/src/Grace.Server/Startup.Server.fs index 8e13d8e..7a90da6 100644 --- a/src/Grace.Server/Startup.Server.fs +++ b/src/Grace.Server/Startup.Server.fs @@ -172,6 +172,7 @@ module Application = route "/setCheckpointDays" Repository.SetCheckpointDays |> addMetadata typeof route "/setDefaultServerApiVersion" Repository.SetDefaultServerApiVersion |> addMetadata typeof route "/setDescription" Repository.SetDescription |> addMetadata typeof + route "/setName" Repository.SetName |> addMetadata typeof route "/setRecordSaves" Repository.SetRecordSaves |> addMetadata typeof route "/setSaveDays" Repository.SetSaveDays |> addMetadata typeof route "/setStatus" Repository.SetStatus |> addMetadata typeof diff --git a/src/Grace.Server/Validations.Server.fs b/src/Grace.Server/Validations.Server.fs index 596ffde..48a45fb 100644 --- a/src/Grace.Server/Validations.Server.fs +++ b/src/Grace.Server/Validations.Server.fs @@ -11,16 +11,18 @@ open Grace.Actors.Services open Grace.Shared.Constants open Grace.Shared.Types open Grace.Shared.Utilities +open Grace.Shared.Validation open Microsoft.AspNetCore.Http +open Microsoft.Extensions.Logging open System open System.Collections.Concurrent open System.Collections.Generic open System.Threading.Tasks -open ApplicationContext module Validations = let actorProxyFactory = ApplicationContext.actorProxyFactory + let log = ApplicationContext.loggerFactory.CreateLogger("Validations.Server") let memoryCache = ApplicationContext.memoryCache module Owner = @@ -39,7 +41,7 @@ module Validations = let ownerActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Owner) let! exists = ownerActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(ownerGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(ownerGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error @@ -79,7 +81,7 @@ module Validations = let ownerActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Owner) let! exists = ownerActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(ownerGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(ownerGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error @@ -161,7 +163,7 @@ module Validations = let organizationActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Organization) let! exists = organizationActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(organizationGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(organizationGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error @@ -200,35 +202,39 @@ module Validations = /// Validates that the organization exists. let organizationExists<'T> ownerId ownerName organizationId organizationName (error: 'T) = task { - let mutable organizationGuid = Guid.Empty - match! resolveOrganizationId ownerId ownerName organizationId organizationName with - | Some organizationId -> - if Guid.TryParse(organizationId, &organizationGuid) then - let mutable x = null - let cached = memoryCache.TryGetValue(organizationGuid, &x) - if cached then - return Ok () - else - let actorId = Organization.GetActorId(organizationGuid) - let organizationActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Organization) - let! exists = organizationActorProxy.Exists() - if exists then - use newCacheEntry = memoryCache.CreateEntry(organizationGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + try + let mutable organizationGuid = Guid.Empty + match! resolveOrganizationId ownerId ownerName organizationId organizationName with + | Some organizationId -> + if Guid.TryParse(organizationId, &organizationGuid) then + let mutable x = null + let cached = memoryCache.TryGetValue(organizationGuid, &x) + if cached then return Ok () else - return Error error - else - return Ok () - | None -> return Error error + let actorId = Organization.GetActorId(organizationGuid) + let organizationActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Organization) + let! exists = organizationActorProxy.Exists() + if exists then + use newCacheEntry = memoryCache.CreateEntry(organizationGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) + return Ok () + else + return Error error + else + return Ok () + | None -> return Error error + with ex -> + log.LogError(ex, "{currentInstant}: Exception in Grace.Server.Validations.organizationExists.", getCurrentInstantExtended()) + return Error error } /// Validates that the organization does not exist. let organizationDoesNotExist<'T> ownerId ownerName organizationId organizationName (error: 'T) = task { - match! resolveOrganizationId ownerId ownerName organizationId organizationName with - | Some organizationId -> - return Error error - | None -> return Ok () + match! organizationExists ownerId ownerName organizationId organizationName error with + | Ok _ -> + return Error error + | Error error -> return Ok () } /// Validates that the organization is deleted. @@ -277,7 +283,7 @@ module Validations = let repositoryActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Repository) let! exists = repositoryActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(repositoryGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(repositoryGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error @@ -308,11 +314,10 @@ module Validations = if cached then return Ok () else - let actorId = ActorId($"{repositoryGuid}") - let repositoryActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Repository) + let repositoryActorProxy = actorProxyFactory.CreateActorProxy(ActorId(repositoryId), ActorName.Repository) let! exists = repositoryActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(repositoryGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(repositoryGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error @@ -359,6 +364,22 @@ module Validations = | None -> return Error error } + let repositoryNameIsUnique<'T> ownerId ownerName organizationId organizationName repositoryName (error: 'T) = + task { + if not <| String.IsNullOrEmpty(repositoryName) then + match! repositoryNameIsUnique ownerId ownerName organizationId organizationName repositoryName with + | Ok isUnique -> + if isUnique then + return Ok () + else + return Error error + | Error internalError -> + logToConsole internalError + return Error error + else + return Ok () + } + module Branch = /// Validates that the given branchId exists in the database. @@ -375,7 +396,7 @@ module Validations = let branchActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Branch) let! exists = branchActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(branchGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(branchGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error @@ -418,7 +439,7 @@ module Validations = let branchActorProxy = actorProxyFactory.CreateActorProxy(actorId, ActorName.Branch) let! exists = branchActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(branchGuid, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(branchGuid, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error @@ -493,7 +514,7 @@ module Validations = let directoryVersionActorProxy = ApplicationContext.actorProxyFactory.CreateActorProxy(actorId, ActorName.DirectoryVersion) let! exists = directoryVersionActorProxy.Exists() if exists then - use newCacheEntry = memoryCache.CreateEntry(directoryId, Value = null, SlidingExpiration = DefaultExpirationTime) + use newCacheEntry = memoryCache.CreateEntry(directoryId, Value = null, AbsoluteExpirationRelativeToNow = DefaultExpirationTime) return Ok () else return Error error diff --git a/src/Grace.Shared/Resources/Text/Languages.Resources.fs b/src/Grace.Shared/Resources/Text/Languages.Resources.fs index 8860738..94366ce 100644 --- a/src/Grace.Shared/Resources/Text/Languages.Resources.fs +++ b/src/Grace.Shared/Resources/Text/Languages.Resources.fs @@ -29,6 +29,7 @@ module Text = | EitherOrganizationIdOrOrganizationNameIsRequired | EitherOwnerIdOrOwnerNameIsRequired | EitherRepositoryIdOrRepositoryNameIsRequired + | ExceptionCaught | FailedCommunicatingWithObjectStorage | FailedCreatingInitialBranch | FailedRebasingInitialBranch @@ -110,6 +111,7 @@ module Text = | RepositoryIsNotDeleted | RepositoryIsNotEmpty | RepositoryNameIsRequired + | RepositoryNameAlreadyExists | SaveIsDisabled | SavingDirectoryVersions | ScanningWorkingDirectory diff --git a/src/Grace.Shared/Resources/Text/en-US.fs b/src/Grace.Shared/Resources/Text/en-US.fs index 0ea9db6..60eddc6 100644 --- a/src/Grace.Shared/Resources/Text/en-US.fs +++ b/src/Grace.Shared/Resources/Text/en-US.fs @@ -33,6 +33,7 @@ module en_US = | EitherOrganizationIdOrOrganizationNameIsRequired -> "Either a OrganizationId or a OrganizationName must be provided. If both are provided, OrganizationId will be used." | EitherOwnerIdOrOwnerNameIsRequired -> "Either an OwnerId or an OwnerName must be provided. If both are provided, OwnerId will be used." | EitherRepositoryIdOrRepositoryNameIsRequired -> "Either a RepositoryId, or a RepositoryName, must be provided. If both are provided, RepositoryId will be used." + | ExceptionCaught -> "An exception was caught while processing the request." | FailedCommunicatingWithObjectStorage -> "A failure occurred when communicating with object storage." | FailedCreatingInitialBranch -> "A server error occurred while attempting to create the initial branch." | FailedCreatingInitialPromotion -> "A server error occurred while attempting to create the initial promotion." @@ -114,6 +115,7 @@ module en_US = | RepositoryIsNotDeleted -> "The repository is not deleted." | RepositoryIsNotEmpty -> "The repository is not empty. Only empty repositories can be initialized." | RepositoryNameIsRequired -> "The RepositoryName must be provided." + | RepositoryNameAlreadyExists -> "A repository with the provided name already exists." | SaveIsDisabled -> "This branch has disabled saves." | SavingDirectoryVersions -> "Saving the new directory versions on the server." | ScanningWorkingDirectory -> "Scanning your working directory for changes." diff --git a/src/Grace.Shared/Validation/Errors.Validation.fs b/src/Grace.Shared/Validation/Errors.Validation.fs index 7269515..8714206 100644 --- a/src/Grace.Shared/Validation/Errors.Validation.fs +++ b/src/Grace.Shared/Validation/Errors.Validation.fs @@ -238,6 +238,7 @@ module Errors = | DuplicateCorrelationId | EitherOrganizationIdOrOrganizationNameRequired | EitherOwnerIdOrOwnerNameRequired + | ExceptionCaught | FailedWhileApplyingEvent | FailedWhileSavingEvent | InvalidOrganizationId @@ -271,6 +272,7 @@ module Errors = | DuplicateCorrelationId -> getLocalizedString StringResourceName.DuplicateCorrelationId | EitherOrganizationIdOrOrganizationNameRequired -> getLocalizedString StringResourceName.EitherOrganizationIdOrOrganizationNameIsRequired | EitherOwnerIdOrOwnerNameRequired -> getLocalizedString StringResourceName.EitherOwnerIdOrOwnerNameIsRequired + | ExceptionCaught -> getLocalizedString StringResourceName.ExceptionCaught | FailedWhileApplyingEvent -> getLocalizedString StringResourceName.FailedWhileApplyingEvent | FailedWhileSavingEvent -> getLocalizedString StringResourceName.FailedWhileSavingEvent | InvalidOrganizationId -> getLocalizedString StringResourceName.InvalidOrganizationId @@ -344,6 +346,7 @@ module Errors = | RepositoryIsDeleted | RepositoryIsNotDeleted | RepositoryIsNotEmpty + | RepositoryNameAlreadyExists static member getErrorMessage (repositoryError: RepositoryError): string = match repositoryError with @@ -387,6 +390,7 @@ module Errors = | RepositoryIsNotDeleted -> getLocalizedString StringResourceName.RepositoryIsNotDeleted | RepositoryIsNotEmpty -> getLocalizedString StringResourceName.RepositoryIsNotEmpty | RepositoryNameIsRequired -> getLocalizedString StringResourceName.RepositoryNameIsRequired + | RepositoryNameAlreadyExists -> getLocalizedString StringResourceName.RepositoryNameAlreadyExists static member getErrorMessage (repositoryError: RepositoryError option): string =