From 58362be2837ead35592dfb2de14cd762d99ec929 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 2 Mar 2021 17:22:37 +0100 Subject: [PATCH] Feature/status (#663) Bulk endpoint improvements. --- .../CreateContentActionHandler.cs | 5 +- .../Discourse/DiscourseActionHandler.cs | 6 +- backend/i18n/source/backend_en.json | 4 +- backend/i18n/source/backend_it.json | 2 - backend/i18n/source/backend_nl.json | 2 - .../src/Migrations/OldEvents/AssetCreated.cs | 2 +- .../src/Migrations/OldEvents/AssetUpdated.cs | 2 +- .../Apps/AppPlan.cs | 17 +- .../Apps/LanguageConfig.cs | 2 +- .../Apps/LanguagesConfig.cs | 6 +- .../Apps/Role.cs | 2 +- .../Apps/Roles.cs | 8 +- .../Comments/Comment.cs | 26 +- .../Contents/ContentData.cs | 2 +- .../Contents/Status.cs | 2 +- .../Freezable.cs | 2 +- .../InvariantPartitioning.cs | 2 +- .../EnrichedEvents/EnrichedAssetEvent.cs | 4 +- .../EnrichedEvents/EnrichedCommentEvent.cs | 2 +- .../EnrichedEvents/EnrichedContentEvent.cs | 2 +- .../EnrichedEvents/EnrichedManualEvent.cs | 2 +- .../EnrichedEvents/EnrichedSchemaEvent.cs | 4 +- .../EnrichedUsageExceededEvent.cs | 2 +- .../Rules/Rule.cs | 8 +- .../Schemas/ArrayField.cs | 8 +- .../Schemas/FieldCollection.cs | 2 +- .../Schemas/NestedField.cs | 10 +- .../Schemas/NestedField{T}.cs | 4 +- .../Schemas/RootField.cs | 12 +- .../Schemas/RootField{T}.cs | 4 +- .../Schemas/Schema.cs | 28 +- .../GenerateJsonSchema/JsonTypeVisitor.cs | 4 +- .../HandleRules/EventJsonSchemaGenerator.cs | 2 +- .../HandleRules/RuleActionHandler.cs | 4 +- .../HandleRules/RuleRegistry.cs | 2 +- .../HandleRules/RuleTriggerHandler.cs | 4 +- .../ContentWrapper/ContentDataProperty.cs | 2 +- .../ContentWrapper/ContentFieldObject.cs | 2 +- .../ContentWrapper/ContentFieldProperty.cs | 4 +- .../ValidateContent/ContentValidator.cs | 2 +- .../DefaultFieldValueValidatorsFactory.cs | 18 +- .../ValidateContent/ValidationContext.cs | 1 + .../Validators/AssetsValidator.cs | 18 +- .../Validators/CollectionValidator.cs | 8 +- .../Validators/RangeValidator.cs | 8 +- .../Validators/ReferencesValidator.cs | 2 +- .../Validators/StringLengthValidator.cs | 6 +- .../Validators/StringTextValidator.cs | 8 +- .../Assets/MongoAssetEntity.cs | 4 +- .../Assets/MongoAssetFolderEntity.cs | 2 +- .../Assets/MongoAssetFolderRepository.cs | 2 +- ...ongoAssetFolderRepository_SnapshotStore.cs | 2 +- .../MongoAssetRepository_SnapshotStore.cs | 2 +- .../Assets/Visitors/FindExtensions.cs | 2 +- .../Contents/MongoContentEntity.cs | 2 +- .../MongoContentRepository_SnapshotStore.cs | 20 +- .../FullText/MongoTextIndex.cs | 7 +- .../Rules/MongoRuleEventEntity.cs | 4 +- .../Rules/MongoRuleEventRepository.cs | 2 +- .../Schemas/MongoSchemasHash.cs | 8 +- .../Apps/Commands/AppUpdateCommand.cs | 2 +- .../Apps/Commands/CreateApp.cs | 2 +- .../Apps/DomainObject/AppCommandMiddleware.cs | 9 +- .../DomainObject/AppDomainObject.State.cs | 2 +- .../Apps/DomainObject/AppDomainObject.cs | 60 +-- .../Invitation/InvitationEventConsumer.cs | 2 +- .../Apps/Plans/NoopAppPlanBillingManager.cs | 2 +- .../Templates/CreateBlogCommandMiddleware.cs | 4 +- .../Assets/AssetChangedTriggerHandler.cs | 10 +- ...ssetCreatedResult.cs => AssetDuplicate.cs} | 14 +- .../Assets/AssetEntity.cs | 4 +- .../Assets/AssetUsageTracker_EventHandling.cs | 8 +- .../Assets/Commands/AssetCommand.cs | 2 +- .../Assets/Commands/AssetFolderCommand.cs | 2 +- .../Assets/Commands/BulkUpdateAssetType.cs} | 8 +- .../Assets/Commands/BulkUpdateAssets.cs} | 13 +- .../Assets/Commands/BulkUpdateJob.cs | 38 ++ .../Assets/Commands/CreateAsset.cs | 9 +- .../Assets/Commands/DeleteAsset.cs | 2 + .../Assets/Commands/MoveAsset.cs | 2 + .../Assets/Commands/UploadAssetCommand.cs | 3 + .../Assets/Commands/UpsertAsset.cs | 39 ++ .../DomainObject/AssetCommandMiddleware.cs | 127 ++++-- .../DomainObject/AssetDomainObject.State.cs | 20 +- .../Assets/DomainObject/AssetDomainObject.cs | 128 ++++-- .../DomainObject/AssetDomainObjectGrain.cs | 2 +- .../AssetFolderDomainObject.State.cs | 2 +- .../DomainObject/AssetFolderDomainObject.cs | 36 +- .../DomainObject/AssetFolderResolver.cs | 117 +++++ .../AssetsBulkUpdateCommandMiddleware.cs | 210 +++++++++ .../Assets/DomainObject/Guards/GuardAsset.cs | 7 +- .../DomainObject/IAssetFolderResolver.cs} | 18 +- .../Assets/FileTagAssetMetadataSource.cs | 19 +- .../Assets/FileTypeAssetMetadataSource.cs | 6 +- .../Assets/IAssetMetadataSource.cs | 2 +- .../Assets/ImageAssetMetadataSource.cs | 12 +- .../Assets/RecursiveDeleter.cs | 4 +- .../Backup/BackupContextBase.cs | 2 +- .../Backup/BackupGrain.cs | 2 +- .../Backup/BackupReader.cs | 4 +- .../Backup/BackupWriter.cs | 4 +- .../Backup/RestoreGrain.cs | 2 +- .../Backup/UserMapping.cs | 2 +- .../{Contents => }/BulkUpdateResult.cs | 2 +- .../{Contents => }/BulkUpdateResultItem.cs | 4 +- .../Comments/DomainObject/CommentsGrain.cs | 24 +- .../Comments/DomainObject/ICommentsGrain.cs | 3 +- ...UpdateType.cs => BulkUpdateContentType.cs} | 2 +- .../Contents/Commands/BulkUpdateContents.cs | 2 + .../Contents/Commands/BulkUpdateJob.cs | 6 +- .../Contents/Commands/ChangeContentStatus.cs | 4 + .../Contents/Commands/ContentCommand.cs | 2 +- .../Contents/Commands/ContentDataCommand.cs | 6 +- .../Contents/Commands/ContentUpdateCommand.cs | 13 - .../Contents/Commands/CreateContent.cs | 9 +- .../Contents/Commands/DeleteContent.cs | 2 + .../Contents/Commands/PatchContent.cs | 2 +- .../Contents/Commands/UpdateContent.cs | 2 +- .../Contents/Commands/UpsertContent.cs | 21 +- .../Contents/ContentChangedTriggerHandler.cs | 28 +- .../Contents/ContentEntity.cs | 2 +- .../Contents/DefaultContentWorkflow.cs | 12 +- .../DomainObject/ContentCommandMiddleware.cs | 15 +- .../DomainObject/ContentDomainObject.State.cs | 10 +- .../DomainObject/ContentDomainObject.cs | 347 +++++++------- .../DomainObject/ContentDomainObjectGrain.cs | 2 +- .../DomainObject/ContentOperationContext.cs | 6 +- .../ContentsBulkUpdateCommandMiddleware.cs} | 72 +-- .../DomainObject/Guards/GuardContent.cs | 128 +++--- .../Contents/DynamicContentWorkflow.cs | 11 +- .../Contents/GraphQL/CachingGraphQLService.cs | 2 +- .../Contents/GraphQL/Types/Builder.cs | 2 +- .../GraphQL/Types/Contents/ContentActions.cs | 45 +- .../Types/Primitives/EntitySavedGraphType.cs | 4 +- .../Contents/IContentWorkflow.cs | 4 +- .../Contents/Queries/ContentEnricher.cs | 2 +- .../Queries/Steps/EnrichWithWorkflows.cs | 2 +- .../Queries/Steps/ResolveReferences.cs | 2 +- .../Contents/SingletonCommandMiddleware.cs | 4 +- .../Contents/Text/TextIndexingProcess.cs | 10 +- .../Squidex.Domain.Apps.Entities/Context.cs | 18 +- .../History/HistoryEventsCreatorBase.cs | 2 +- .../History/HistoryService.cs | 6 +- .../History/ParsedHistoryEvent.cs | 14 +- .../Notifications/NoopNotificationSender.cs | 2 +- .../Notifications/NotificationEmailSender.cs | 2 +- .../Rules/Commands/RuleCommand.cs | 2 +- .../Guards/RuleTriggerValidator.cs | 2 +- .../DomainObject/RuleDomainObject.State.cs | 2 +- .../Rules/DomainObject/RuleDomainObject.cs | 32 +- .../Rules/RuleCommandMiddleware.cs | 15 +- .../Rules/RuleDequeuerGrain.cs | 4 +- .../Rules/RuleEnqueuer.cs | 2 +- .../Rules/RuleEntity.cs | 2 +- .../Rules/Runner/RuleRunnerGrain.cs | 2 +- .../Rules/UsageTracking/UsageTrackerGrain.cs | 4 +- .../Schemas/Commands/CreateSchema.cs | 2 +- .../Schemas/Commands/SchemaUpdateCommand.cs | 2 +- .../DomainObject/Guards/GuardHelper.cs | 2 +- .../DomainObject/Guards/GuardSchema.cs | 2 +- .../DomainObject/Guards/GuardSchemaField.cs | 2 +- .../DomainObject/SchemaDomainObject.State.cs | 2 +- .../DomainObject/SchemaDomainObject.cs | 64 +-- .../MongoUserStore.cs | 2 +- .../Squidex.Domain.Users/UserWithClaims.cs | 6 +- .../EventSourcing/CosmosDbEventStore.cs | 2 +- .../EventSourcing/StreamPosition.cs | 2 +- .../EventSourcing/MongoEventStore.cs | 4 +- .../MongoDb/DomainIdSerializer.cs | 2 +- .../MongoDb/InstantSerializer.cs | 2 +- .../MongoDb/MongoRepositoryBase.cs | 2 +- .../States/MongoSnapshotStore.cs | 2 +- .../CQRS/Events/RabbitMqEventConsumer.cs | 4 +- .../ImmutableDictionary{TKey,TValue}.cs | 6 +- .../Commands/CommandContext.cs | 15 +- .../Commands/CommandRequest.cs | 18 +- .../Commands/CommandResult.cs | 23 + .../Commands/DomainObject.Execute.cs | 261 +++++++++++ .../Commands/DomainObject.cs | 296 ++++++++++-- .../Commands/DomainObjectBase.cs | 267 ----------- .../Commands/DomainObjectGrain.cs | 8 +- .../Commands/EntityCreatedResult.cs | 17 - .../Commands/EntityCreatedResult{T}.cs | 20 - .../Commands/GrainCommandMiddleware.cs | 11 +- .../Commands/IDomainObjectGrain.cs | 2 +- .../src/Squidex.Infrastructure/Commands/Is.cs | 14 +- .../Commands/LogSnapshotDomainObject.cs | 127 ------ .../Commands/Rebuilder.cs | 6 +- .../Commands/SnapshotList.cs | 124 +++++ .../DomainObjectException.cs | 2 +- .../src/Squidex.Infrastructure/EtagVersion.cs | 2 - .../EventSourcing/Envelope{T}.cs | 4 +- .../EventSourcing/Grains/BatchSubscriber.cs | 2 +- .../Grains/EventConsumerState.cs | 4 +- .../Json/Newtonsoft/JsonValueConverter.cs | 2 +- .../Json/Objects/JsonArray.cs | 2 +- .../Json/Objects/JsonBoolean.cs | 2 +- .../Json/Objects/JsonNull.cs | 2 +- .../Json/Objects/JsonNumber.cs | 2 +- .../Json/Objects/JsonObject.cs | 8 +- .../Json/Objects/JsonString.cs | 2 +- .../src/Squidex.Infrastructure/Language.cs | 4 +- .../Orleans/GrainState.cs | 2 +- .../Orleans/StreamReaderWrapper.cs | 6 +- .../Orleans/StreamWriterWrapper.cs | 8 +- .../Queries/OData/LimitExtensions.cs | 4 +- .../Squidex.Infrastructure/Queries/Query.cs | 2 +- .../src/Squidex.Infrastructure/RefToken.cs | 4 +- .../Security/Permission.cs | 2 +- .../Security/PermissionSet.cs | 2 +- .../States/IPersistence{TState}.cs | 2 +- .../Squidex.Infrastructure/States/IStore.cs | 2 +- .../States/Persistence.cs | 4 +- .../States/PersistenceMode.cs | 9 +- .../States/Persistence{TSnapshot,TKey}.cs | 175 ++++---- .../Squidex.Infrastructure/States/Store.cs | 17 +- .../Tasks/PartitionedActionBlock.cs | 12 +- .../Tasks/TaskExtensions.cs | 46 -- .../Validation/ValidationError.cs | 4 +- backend/src/Squidex.Shared/Permissions.cs | 4 +- backend/src/Squidex.Shared/Texts.it.resx | 6 +- backend/src/Squidex.Shared/Texts.nl.resx | 6 +- backend/src/Squidex.Shared/Texts.resx | 6 +- .../src/Squidex.Shared/Users/ClientUser.cs | 8 +- backend/src/Squidex.Web/ApiController.cs | 6 +- .../src/Squidex.Web/ApiPermissionAttribute.cs | 2 +- .../ETagCommandMiddleware.cs | 4 +- backend/src/Squidex.Web/Deferred.cs | 2 +- backend/src/Squidex.Web/EntityCreatedDto.cs | 28 -- .../Pipeline/FileCallbackResultExecutor.cs | 2 +- .../Squidex.Web/Pipeline/UsagePipeWriter.cs | 2 +- .../Pipeline/UsageResponseBodyFeature.cs | 6 +- .../src/Squidex.Web/Pipeline/UsageStream.cs | 8 +- .../Squidex.Web/Services/StringLocalizer.cs | 2 +- .../Api/Config/OpenApi/XmlTagProcessor.cs | 29 +- .../Assets/AssetFoldersController.cs | 14 +- .../Controllers/Assets/AssetsController.cs | 74 ++- .../Assets/Models/AssetContentQueryDto.cs | 4 +- .../Api/Controllers/Assets/Models/AssetDto.cs | 6 +- .../Assets/Models/BulkUpdateAssetsDto.cs | 32 ++ .../Assets/Models/BulkUpdateAssetsJobDto.cs | 78 ++++ .../{MoveAssetItemDto.cs => MoveAssetDto.cs} | 15 +- .../Assets/Models/MoveAssetFolderDto.cs | 26 ++ .../{Contents/Models => }/BulkResultDto.cs | 19 +- .../Contents/ContentsController.cs | 47 +- ...kUpdateDto.cs => BulkUpdateContentsDto.cs} | 30 +- ...eJobDto.cs => BulkUpdateContentsJobDto.cs} | 13 +- .../Controllers/Contents/Models/ContentDto.cs | 2 +- .../Contents/Models/CreateContentDto.cs | 67 +++ .../Contents/Models/ImportContentsDto.cs | 22 +- .../Contents/Models/UpsertContentDto.cs | 56 +++ .../Controllers/Rules/Models/RuleEventDto.cs | 2 +- .../Frontend/Middlewares/IndexExtensions.cs | 2 +- .../Squidex/Config/Domain/AssetServices.cs | 3 + .../Squidex/Config/Domain/CommandsServices.cs | 7 +- backend/src/Squidex/Config/Web/WebServices.cs | 1 + .../Scripting/JintScriptEngineTests.cs | 2 +- .../DomainObject/AppCommandMiddlewareTests.cs | 45 +- .../Apps/DomainObject/AppDomainObjectTests.cs | 31 +- .../Apps/Indexes/AppsIndexIntegrationTests.cs | 16 +- .../Apps/Indexes/AppsIndexTests.cs | 54 +-- .../AssetCommandMiddlewareTests.cs | 221 ++++----- .../DomainObject/AssetDomainObjectTests.cs | 134 ++++-- .../AssetFolderDomainObjectTests.cs | 29 +- .../DomainObject/AssetFolderResolverTests.cs | 165 +++++++ .../AssetsBulkUpdateCommandMiddlewareTests.cs | 208 +++++++++ .../DomainObject/Guards/GuardAssetTests.cs | 27 +- .../Assets/FileTagAssetMetadataSourceTests.cs | 8 +- .../FileTypeAssetMetadataSourceTests.cs | 14 +- .../Assets/ImageAssetMetadataSourceTests.cs | 30 +- .../CommentsCommandMiddlewareTests.cs | 21 +- .../DomainObject/CommentsGrainTests.cs | 25 +- .../ContentChangedTriggerHandlerTests.cs | 3 +- .../Contents/DefaultContentWorkflowTests.cs | 26 +- .../ContentCommandMiddlewareTests.cs | 50 ++- .../DomainObject/ContentDomainObjectTests.cs | 257 +++++++++-- ...ntentsBulkUpdateCommandMiddlewareTests.cs} | 130 +++--- .../DomainObject/Guards/GuardContentTests.cs | 114 ++--- .../Contents/DynamicContentWorkflowTests.cs | 32 +- .../Contents/GraphQL/GraphQLMutationTests.cs | 167 ++++--- .../Contents/GraphQL/GraphQLQueriesTests.cs | 115 ++--- .../Queries/EnrichWithWorkflowsTests.cs | 2 +- .../SingletonCommandMiddlewareTests.cs | 3 +- .../RuleCommandMiddlewareTests.cs | 50 ++- .../DomainObject/RuleDomainObjectTests.cs | 41 +- .../DomainObject/SchemaDomainObjectTests.cs | 10 +- .../Schemas/Indexes/SchemasIndexTests.cs | 2 +- .../TestHelpers/HandlerTestBase.cs | 46 +- .../Commands/DomainObjectTests.cs | 351 ++++++++++----- .../Commands/LogSnapshotDomainObjectTests.cs | 423 ------------------ .../DomainIdTests.cs | 2 +- .../EventSourcing/EventStoreTests.cs | 2 +- .../EventSourcing/MongoParallelInsertTests.cs | 2 +- .../Reflection/PropertiesTypeAccessorTests.cs | 4 +- .../Reflection/SimpleEqualsTests.cs | 2 +- .../Reflection/SimpleMapperTests.cs | 2 +- .../States/PersistenceEventSourcingTests.cs | 152 +++++-- .../States/PersistenceSnapshotTests.cs | 40 +- .../States/Save.cs | 57 +++ .../Tasks/PartitionedActionBlockTests.cs | 2 + .../TestHelpers/MyDomainObject.cs | 98 +++- .../TestHelpers/MyDomainState.cs | 19 +- .../ETagCommandMiddlewareTests.cs | 52 +-- .../EnrichWithActorCommandMiddlewareTests.cs | 47 +- .../EnrichWithAppIdCommandMiddlewareTests.cs | 28 +- ...nrichWithSchemaIdCommandMiddlewareTests.cs | 44 +- .../pages/content/content-page.component.ts | 2 + .../contributor-add-form.component.ts | 2 +- 308 files changed, 4755 insertions(+), 3177 deletions(-) rename backend/src/Squidex.Domain.Apps.Entities/Assets/{AssetCreatedResult.cs => AssetDuplicate.cs} (58%) rename backend/src/{Squidex.Infrastructure/EventSourcing/IEventEnricher.cs => Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs} (73%) rename backend/src/{Squidex.Infrastructure/Commands/EntitySavedResult.cs => Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs} (59%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderResolver.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs rename backend/src/{Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs => Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderResolver.cs} (54%) rename backend/src/Squidex.Domain.Apps.Entities/{Contents => }/BulkUpdateResult.cs (93%) rename backend/src/Squidex.Domain.Apps.Entities/{Contents => }/BulkUpdateResultItem.cs (85%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/{BulkUpdateType.cs => BulkUpdateContentType.cs} (93%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/{BulkUpdateCommandMiddleware.cs => DomainObject/ContentsBulkUpdateCommandMiddleware.cs} (76%) create mode 100644 backend/src/Squidex.Infrastructure/Commands/CommandResult.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs delete mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs delete mode 100644 backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs delete mode 100644 backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs delete mode 100644 backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs delete mode 100644 backend/src/Squidex.Web/EntityCreatedDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs rename backend/src/Squidex/Areas/Api/Controllers/Assets/Models/{MoveAssetItemDto.cs => MoveAssetDto.cs} (71%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs rename backend/src/Squidex/Areas/Api/Controllers/{Contents/Models => }/BulkResultDto.cs (66%) rename backend/src/Squidex/Areas/Api/Controllers/Contents/Models/{BulkUpdateDto.cs => BulkUpdateContentsDto.cs} (64%) rename backend/src/Squidex/Areas/Api/Controllers/Contents/Models/{BulkUpdateJobDto.cs => BulkUpdateContentsJobDto.cs} (87%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderResolverTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/{BulkUpdateCommandMiddlewareTests.cs => DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs} (72%) delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/States/Save.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs index 88765f5f5..c53bb0fe7 100644 --- a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs @@ -68,7 +68,10 @@ namespace Squidex.Extensions.Actions.CreateContent ruleJob.Actor = userEvent.Actor; } - ruleJob.Publish = action.Publish; + if (action.Publish) + { + ruleJob.Status = Status.Published; + } return (Description, ruleJob); } diff --git a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs index 5d1576ab7..2f38070f8 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs @@ -37,12 +37,12 @@ namespace Squidex.Extensions.Actions.Discourse ["title"] = await FormatAsync(action.Title, @event) }; - if (action.Topic.HasValue) + if (action.Topic != null) { json.Add("topic_id", action.Topic.Value); } - if (action.Category.HasValue) + if (action.Category != null) { json.Add("category", action.Category.Value); } @@ -58,7 +58,7 @@ namespace Squidex.Extensions.Actions.Discourse }; var description = - action.Topic.HasValue ? + action.Topic != null ? DescriptionCreateTopic : DescriptionCreatePost; diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 0753d8c45..935b70e91 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -137,7 +137,7 @@ "contents.singletonNotChangeable": "Singleton content cannot be updated.", "contents.singletonNotCreatable": "Singleton content cannot be created.", "contents.singletonNotDeletable": "Singleton content cannot be deleted.", - "contents.statusSchedulingNotInFuture": "Due time must be in the future.", + "contents.statusNotValid": "Status is not defined in the workflow.", "contents.statusTransitionNotAllowed": "Cannot change status from {oldStatus} to {newStatus}.", "contents.validation.aspectRatio": "Must have aspect ratio {width}:{height}.", "contents.validation.assetNotFound": "Id {id} not found.", @@ -173,7 +173,6 @@ "contents.validation.normalCharactersBetween": "Must have between {min} and {max} text character(s).", "contents.validation.notAllowed": "Not an allowed value.", "contents.validation.pattern": "Must follow the pattern.", - "contents.validation.reference": "Geolocation can only have latitude and longitude property.", "contents.validation.referenceNotFound": "Reference '{id}' not found.", "contents.validation.referenceToInvalidSchema": "Reference '{id}' has invalid schema.", "contents.validation.regexTooSlow": "Regex is too slow.", @@ -182,7 +181,6 @@ "contents.validation.unknownField": "Not a known {fieldType}.", "contents.validation.wordCount": "Must have exactly {count} word(s).", "contents.validation.wordsBetween": "Must have between {min} and {max} word(s).", - "contents.workflowErorPublishing": "Content workflow prevents publishing.", "contents.workflowErrorUpdate": "The workflow does not allow updates at status {status}", "dotnet_identity_DefaultEror": "An unknown failure has occurred.", "dotnet_identity_DuplicateEmail": "Email is already taken.", diff --git a/backend/i18n/source/backend_it.json b/backend/i18n/source/backend_it.json index f019ea154..7b916255e 100644 --- a/backend/i18n/source/backend_it.json +++ b/backend/i18n/source/backend_it.json @@ -137,7 +137,6 @@ "contents.singletonNotChangeable": "Il contenuto singleton non può essere aggiornato", "contents.singletonNotCreatable": "Il contenuto singleton non può essere creato.", "contents.singletonNotDeletable": "Il contenuto singleton non può essere eliminato.", - "contents.statusSchedulingNotInFuture": "L'ora deve essere futura.", "contents.statusTransitionNotAllowed": "Non è possibile cambiare stato da {oldStatus} a {newStatus}.", "contents.validation.aspectRatio": "Deve essere le proporzioni {width}:{height}.", "contents.validation.assetNotFound": "Id {id} non trovato.", @@ -182,7 +181,6 @@ "contents.validation.unknownField": "Non è noto {fieldType}.", "contents.validation.wordCount": "Deve avere esattamente {count} parola(e).", "contents.validation.wordsBetween": "Deve essere tra {min} e {max} parola(e).", - "contents.workflowErorPublishing": "Il workflow del contenuto impedisce la pubblicazione.", "contents.workflowErrorUpdate": "Il workflow non consente le modifiche per lo stato {status}", "dotnet_identity_DefaultEror": "Si è verificato un errore sconosciuto.", "dotnet_identity_DuplicateEmail": "Email già in uso.", diff --git a/backend/i18n/source/backend_nl.json b/backend/i18n/source/backend_nl.json index 35ec0f2e0..f77316df3 100644 --- a/backend/i18n/source/backend_nl.json +++ b/backend/i18n/source/backend_nl.json @@ -133,7 +133,6 @@ "contents.singletonNotChangeable": "Singleton-inhoud kan niet worden bijgewerkt.", "contents.singletonNotCreatable": "Singleton-inhoud kan niet worden gemaakt.", "contents.singletonNotDeletable": "Singleton-inhoud kan niet worden verwijderd.", - "contents.statusSchedulingNotInFuture": "De tijd moet in de toekomst liggen.", "contents.statusTransitionNotAllowed": "Kan status niet wijzigen van {oldStatus} in {newStatus}.", "contents.validation.aspectRatio": "Moet aspectverhouding {breedte}: {hoogte} hebben.", "contents.validation.assetNotFound": "Id {id} niet gevonden.", @@ -177,7 +176,6 @@ "contents.validation.unknownField": "Onbekend {fieldType}.", "contents.validation.wordCount": "Moet exact {count} woord (en) bevatten.", "contents.validation.wordsBetween": "Moet tussen {min} en {max} woord (en) bevatten.", - "contents.workflowErorPublishing": "Contentworkflow verhindert publiceren.", "contents.workflowErrorUpdate": "De werkstroom staat geen updates toe met status {status}", "dotnet_identity_DefaultEror": "Er is een onbekende fout opgetreden.", "dotnet_identity_DuplicateEmail": "E-mail is al in gebruik.", diff --git a/backend/src/Migrations/OldEvents/AssetCreated.cs b/backend/src/Migrations/OldEvents/AssetCreated.cs index 382f0e843..a983b87a6 100644 --- a/backend/src/Migrations/OldEvents/AssetCreated.cs +++ b/backend/src/Migrations/OldEvents/AssetCreated.cs @@ -48,7 +48,7 @@ namespace Migrations.OldEvents result.Metadata = new AssetMetadata(); - if (IsImage && PixelWidth.HasValue && PixelHeight.HasValue) + if (IsImage && PixelWidth != null && PixelHeight != null) { result.Type = AssetType.Image; diff --git a/backend/src/Migrations/OldEvents/AssetUpdated.cs b/backend/src/Migrations/OldEvents/AssetUpdated.cs index c66f69d53..04069e628 100644 --- a/backend/src/Migrations/OldEvents/AssetUpdated.cs +++ b/backend/src/Migrations/OldEvents/AssetUpdated.cs @@ -37,7 +37,7 @@ namespace Migrations.OldEvents result.Metadata = new AssetMetadata(); - if (IsImage && PixelWidth.HasValue && PixelHeight.HasValue) + if (IsImage && PixelWidth != null && PixelHeight != null) { result.Type = AssetType.Image; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs index 638848040..9642cf3a3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs @@ -7,22 +7,11 @@ using Squidex.Infrastructure; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Domain.Apps.Core.Apps { - public sealed record AppPlan + public sealed record AppPlan(RefToken Owner, string PlanId) { - public RefToken Owner { get; } - - public string PlanId { get; } - - public AppPlan(RefToken owner, string planId) - { - Guard.NotNull(owner, nameof(owner)); - Guard.NotNullOrEmpty(planId, nameof(planId)); - - Owner = owner; - - PlanId = planId; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs index ef36d27aa..b40b374a6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.Apps public IEnumerable Fallbacks { - get { return fallbacks; } + get => fallbacks; } public LanguageConfig(bool isOptional = false, params Language[]? fallbacks) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index f12d3f0cb..04e4ea1bd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -26,17 +26,17 @@ namespace Squidex.Domain.Apps.Core.Apps public string Master { - get { return master; } + get => master; } public IEnumerable AllKeys { - get { return languages.Keys; } + get => languages.Keys; } public IReadOnlyDictionary Languages { - get { return languages; } + get => languages; } public LanguagesConfig(Dictionary languages, string master) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs index b15f9f213..615b13ce7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Apps [IgnoreDuringEquals] public bool IsDefault { - get { return Roles.IsDefault(this); } + get => Roles.IsDefault(this); } public Role(string name, PermissionSet permissions, JsonObject properties) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index b5ce2d536..50427c814 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -62,22 +62,22 @@ namespace Squidex.Domain.Apps.Core.Apps public int CustomCount { - get { return inner.Count; } + get => inner.Count; } public Role this[string name] { - get { return inner[name]; } + get => inner[name]; } public IEnumerable Custom { - get { return inner.Values; } + get => inner.Values; } public IEnumerable All { - get { return inner.Values.Union(Defaults.Values); } + get => inner.Values.Union(Defaults.Values); } private Roles(ImmutableDictionary roles) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs index 04d8e0226..b74917826 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs @@ -9,31 +9,11 @@ using System; using NodaTime; using Squidex.Infrastructure; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Domain.Apps.Core.Comments { - public sealed class Comment + public sealed record Comment(DomainId Id, Instant Time, RefToken User, string Text, Uri? Url = null) { - public DomainId Id { get; } - - public Instant Time { get; } - - public RefToken User { get; } - - public string Text { get; } - - public Uri? Url { get; } - - public Comment(DomainId id, Instant time, RefToken user, string text, Uri? url = null) - { - Guard.NotEmpty(id, nameof(id)); - Guard.NotNull(user, nameof(user)); - Guard.NotNull(text, nameof(text)); - - Id = id; - Text = text; - Time = time; - User = user; - Url = url; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs index cc02d6852..b8d32308c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Contents { public IEnumerable> ValidValues { - get { return this.Where(x => x.Value != null); } + get => this.Where(x => x.Value != null); } public ContentData() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs index fc48dbaed..c2146fad8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Contents public string Name { - get { return name ?? "Unknown"; } + get => name ?? "Unknown"; } public Status(string? name) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs index 855b2741b..cb2a88e08 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core [IgnoreDuringEquals] public bool IsFrozen { - get { return isFrozen; } + get => isFrozen; } protected void CheckIfFrozen() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs index 8b7cf754d..2ceeb2b49 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core public string Master { - get { return Key; } + get => Key; } public IEnumerable AllKeys diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs index 85b4761e9..083dc1f0a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs @@ -41,12 +41,12 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents public bool IsImage { - get { return AssetType == AssetType.Image; } + get => AssetType == AssetType.Image; } public override long Partition { - get { return Id.GetHashCode(); } + get => Id.GetHashCode(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs index 71b03f164..f4e1f5a61 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents [IgnoreDataMember] public override long Partition { - get { return MentionedUser.Id.GetHashCode(); } + get => MentionedUser.Id.GetHashCode(); } public bool ShouldSerializeMentionedUser() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs index f7f26017e..23884767c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents public override long Partition { - get { return Id.GetHashCode(); } + get => Id.GetHashCode(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs index 9a40dea10..f9f4e924c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs @@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents { public override long Partition { - get { return 0; } + get => 0; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs index d0c57b2ca..46136427b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs @@ -15,12 +15,12 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents public DomainId Id { - get { return SchemaId.Id; } + get => SchemaId.Id; } public override long Partition { - get { return SchemaId.GetHashCode(); } + get => SchemaId.GetHashCode(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs index 0e0ba7702..619b4beda 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents public override long Partition { - get { return AppId.GetHashCode(); } + get => AppId.GetHashCode(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs index efc195503..3cc627c89 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -20,22 +20,22 @@ namespace Squidex.Domain.Apps.Core.Rules public string Name { - get { return name; } + get => name; } public RuleTrigger Trigger { - get { return trigger; } + get => trigger; } public RuleAction Action { - get { return action; } + get => action; } public bool IsEnabled { - get { return isEnabled; } + get => isEnabled; } public Rule(RuleTrigger trigger, RuleAction action) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs index 1c7578075..140f3e269 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs @@ -18,22 +18,22 @@ namespace Squidex.Domain.Apps.Core.Schemas public IReadOnlyList Fields { - get { return fields.Ordered; } + get => fields.Ordered; } public IReadOnlyDictionary FieldsById { - get { return fields.ById; } + get => fields.ById; } public IReadOnlyDictionary FieldsByName { - get { return fields.ByName; } + get => fields.ByName; } public FieldCollection FieldCollection { - get { return fields; } + get => fields; } public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs index d25569c33..3c5170533 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Schemas public IReadOnlyList Ordered { - get { return fieldsOrdered; } + get => fieldsOrdered; } public IReadOnlyDictionary ById diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs index 8bf0a391d..229b572eb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs @@ -20,27 +20,27 @@ namespace Squidex.Domain.Apps.Core.Schemas public long Id { - get { return fieldId; } + get => fieldId; } public string Name { - get { return fieldName; } + get => fieldName; } public bool IsLocked { - get { return isLocked; } + get => isLocked; } public bool IsHidden { - get { return isHidden; } + get => isHidden; } public bool IsDisabled { - get { return isDisabled; } + get => isDisabled; } public abstract FieldProperties RawProperties { get; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs index b4e468880..8bbfb39b2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs @@ -17,12 +17,12 @@ namespace Squidex.Domain.Apps.Core.Schemas public T Properties { - get { return properties; } + get => properties; } public override FieldProperties RawProperties { - get { return properties; } + get => properties; } public NestedField(long id, string name, T? properties = null, IFieldSettings? settings = null) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs index 4693bf75c..ab81703f4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs @@ -21,32 +21,32 @@ namespace Squidex.Domain.Apps.Core.Schemas public long Id { - get { return fieldId; } + get => fieldId; } public string Name { - get { return fieldName; } + get => fieldName; } public bool IsLocked { - get { return isLocked; } + get => isLocked; } public bool IsHidden { - get { return isHidden; } + get => isHidden; } public bool IsDisabled { - get { return isDisabled; } + get => isDisabled; } public Partitioning Partitioning { - get { return partitioning; } + get => partitioning; } public abstract FieldProperties RawProperties { get; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs index 62bf695da..0572b7cd5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs @@ -17,12 +17,12 @@ namespace Squidex.Domain.Apps.Core.Schemas public T Properties { - get { return properties; } + get => properties; } public override FieldProperties RawProperties { - get { return properties; } + get => properties; } public RootField(long id, string name, Partitioning partitioning, T? properties = null, IFieldSettings? settings = null) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index a87032c59..fd32b22a0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -30,72 +30,72 @@ namespace Squidex.Domain.Apps.Core.Schemas public string Name { - get { return name; } + get => name; } public string Category { - get { return category; } + get => category; } public bool IsPublished { - get { return isPublished; } + get => isPublished; } public bool IsSingleton { - get { return isSingleton; } + get => isSingleton; } public IReadOnlyList Fields { - get { return fields.Ordered; } + get => fields.Ordered; } public IReadOnlyDictionary FieldsById { - get { return fields.ById; } + get => fields.ById; } public IReadOnlyDictionary FieldsByName { - get { return fields.ByName; } + get => fields.ByName; } public IReadOnlyDictionary PreviewUrls { - get { return previewUrls; } + get => previewUrls; } public FieldCollection FieldCollection { - get { return fields; } + get => fields; } public FieldRules FieldRules { - get { return fieldRules; } + get => fieldRules; } public FieldNames FieldsInLists { - get { return fieldsInLists; } + get => fieldsInLists; } public FieldNames FieldsInReferences { - get { return fieldsInReferences; } + get => fieldsInReferences; } public SchemaScripts Scripts { - get { return scripts; } + get => scripts; } public SchemaProperties Properties { - get { return properties; } + get => properties; } public Schema(string name, SchemaProperties? properties = null, bool isSingleton = false) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs index 26a2df134..389e48288 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -116,12 +116,12 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { var property = SchemaBuilder.NumberProperty(); - if (field.Properties.MinValue.HasValue) + if (field.Properties.MinValue != null) { property.Minimum = (decimal)field.Properties.MinValue.Value; } - if (field.Properties.MaxValue.HasValue) + if (field.Properties.MaxValue != null) { property.Maximum = (decimal)field.Properties.MaxValue.Value; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs index 1e5db2d49..b2de217b0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules public IReadOnlyCollection AllTypes { - get { return schemas.Value.Keys; } + get => schemas.Value.Keys; } public EventJsonSchemaGenerator(JsonSchemaGenerator schemaGenerator) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs index e9b3fb1f1..f528a92f8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -22,12 +22,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules Type IRuleActionHandler.ActionType { - get { return typeof(TAction); } + get => typeof(TAction); } Type IRuleActionHandler.DataType { - get { return typeof(TData); } + get => typeof(TData); } protected RuleActionHandler(RuleEventFormatter formatter) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs index b1b9c0fab..e83d8809d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules public IReadOnlyDictionary Actions { - get { return actionTypes; } + get => actionTypes; } public RuleRegistry(IEnumerable? registrations = null) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs index c7aa5d7a6..460ae24d1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -25,12 +25,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules public Type TriggerType { - get { return typeof(TTrigger); } + get => typeof(TTrigger); } public virtual bool CanCreateSnapshotEvents { - get { return false; } + get => false; } public virtual async IAsyncEnumerable CreateSnapshotEvents(TTrigger trigger, DomainId appId) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs index 34b6ed511..319fc11bf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper public ContentFieldObject? ContentField { - get { return contentField; } + get => contentField; } public ContentDataProperty(ContentDataObject contentData, ContentFieldObject? contentField = null) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs index 6582f9947..be8ea311c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper public ContentFieldData? FieldData { - get { return fieldData; } + get => fieldData; } public override bool Extensible => true; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs index bfcbc8816..d899333e8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs @@ -47,12 +47,12 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper public IJsonValue ContentValue { - get { return contentValue ??= JsonMapper.Map(value); } + get => contentValue ??= JsonMapper.Map(value); } public bool IsChanged { - get { return isChanged; } + get => isChanged; } public ContentFieldProperty(ContentFieldObject contentField, IJsonValue? contentValue = null) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index 2b4b5a338..5020316f2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IReadOnlyCollection Errors { - get { return errors; } + get => errors; } public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, IEnumerable factories, ISemanticLog log) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs index 4468c71e2..f5af6d2fb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent var isRequired = IsRequired(properties, args.Context); - if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + if (isRequired || properties.MinItems != null || properties.MaxItems != null) { yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new RequiredValidator(); } - if (properties.MinValue.HasValue || properties.MaxValue.HasValue) + if (properties.MinValue != null || properties.MaxValue != null) { yield return new RangeValidator(properties.MinValue, properties.MaxValue); } @@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new RequiredValidator(); } - if (properties.MinValue.HasValue || properties.MaxValue.HasValue) + if (properties.MinValue != null || properties.MaxValue != null) { yield return new RangeValidator(properties.MinValue, properties.MaxValue); } @@ -160,15 +160,15 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new RequiredStringValidator(true); } - if (properties.MinLength.HasValue || properties.MaxLength.HasValue) + if (properties.MinLength != null || properties.MaxLength != null) { yield return new StringLengthValidator(properties.MinLength, properties.MaxLength); } - if (properties.MinCharacters.HasValue || - properties.MaxCharacters.HasValue || - properties.MinWords.HasValue || - properties.MaxWords.HasValue) + if (properties.MinCharacters != null || + properties.MaxCharacters != null || + properties.MinWords != null || + properties.MaxWords != null) { Func? transform = null; @@ -206,7 +206,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent var isRequired = IsRequired(properties, args.Context); - if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + if (isRequired || properties.MinItems != null || properties.MaxItems != null) { yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index 5e8dbc671..a06c1e86e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -32,6 +32,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent : base(appId, schemaId, schema) { JsonSerializer = jsonSerializer; + ContentId = contentId; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index 9fb6b4b98..dabd0ffa8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.properties = properties; - if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + if (isRequired || properties.MinItems != null || properties.MaxItems != null) { collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } @@ -101,14 +101,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators private void ValidateCommon(IAssetInfo asset, ImmutableQueue path, AddError addError) { - if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) + if (properties.MinSize != null && asset.FileSize < properties.MinSize) { var min = properties.MinSize.Value.ToReadableSize(); addError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); } - if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) + if (properties.MaxSize != null && asset.FileSize > properties.MaxSize) { var max = properties.MaxSize.Value.ToReadableSize(); @@ -136,34 +136,34 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators var pixelWidth = asset.Metadata.GetPixelWidth(); var pixelHeight = asset.Metadata.GetPixelHeight(); - if (pixelWidth.HasValue && pixelHeight.HasValue) + if (pixelWidth != null && pixelHeight != null) { var w = pixelWidth.Value; var h = pixelHeight.Value; var actualRatio = (double)w / h; - if (properties.MinWidth.HasValue && w < properties.MinWidth) + if (properties.MinWidth != null && w < properties.MinWidth) { addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); } - if (properties.MaxWidth.HasValue && w > properties.MaxWidth) + if (properties.MaxWidth != null && w > properties.MaxWidth) { addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); } - if (properties.MinHeight.HasValue && h < properties.MinHeight) + if (properties.MinHeight != null && h < properties.MinHeight) { addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); } - if (properties.MaxHeight.HasValue && h > properties.MaxHeight) + if (properties.MaxHeight != null && h > properties.MaxHeight) { addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); } - if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) + if (properties.AspectHeight != null && properties.AspectWidth != null) { var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs index 51ff070ff..3214c25ba 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) { - if (minItems.HasValue && minItems > maxItems) + if (minItems != null && minItems > maxItems) { throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); } @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators return Task.CompletedTask; } - if (minItems.HasValue && maxItems.HasValue) + if (minItems != null && maxItems != null) { if (minItems == maxItems && minItems != items.Count) { @@ -55,12 +55,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } else { - if (minItems.HasValue && items.Count < minItems) + if (minItems != null && items.Count < minItems) { addError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); } - if (maxItems.HasValue && items.Count > maxItems) + if (maxItems != null && items.Count > maxItems) { addError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs index b7e47214c..26567e281 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public RangeValidator(TValue? min, TValue? max) { - if (min.HasValue && max.HasValue && min.Value.CompareTo(max.Value) > 0) + if (min != null && max != null && min.Value.CompareTo(max.Value) > 0) { throw new ArgumentException("Min value must be greater than max value.", nameof(min)); } @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (value is TValue typedValue) { - if (min.HasValue && max.HasValue) + if (min != null && max != null) { if (Equals(min, max) && Equals(min.Value, max.Value)) { @@ -44,12 +44,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } else { - if (min.HasValue && typedValue.CompareTo(min.Value) < 0) + if (min != null && typedValue.CompareTo(min.Value) < 0) { addError(context.Path, T.Get("contents.validation.min", new { min })); } - if (max.HasValue && typedValue.CompareTo(max.Value) > 0) + if (max != null && typedValue.CompareTo(max.Value) > 0) { addError(context.Path, T.Get("contents.validation.max", new { max })); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index 603a89be0..be7b97aec 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.properties = properties; - if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + if (isRequired || properties.MinItems != null || properties.MaxItems != null) { collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs index a7b4eb426..3a0cade17 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) { - if (minLength.HasValue && maxLength.HasValue) + if (minLength != null && maxLength != null) { if (minLength == maxLength && minLength != stringValue.Length) { @@ -44,12 +44,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } else { - if (minLength.HasValue && stringValue.Length < minLength) + if (minLength != null && stringValue.Length < minLength) { addError(context.Path, T.Get("contents.validation.minLength", new { min = minLength })); } - if (maxLength.HasValue && stringValue.Length > maxLength) + if (maxLength != null && stringValue.Length > maxLength) { addError(context.Path, T.Get("contents.validation.maxLength", new { max = maxLength })); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs index d0a06793b..0213ec5b5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs @@ -52,11 +52,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators stringValue = transform(stringValue); } - if (minWords.HasValue || maxWords.HasValue) + if (minWords != null || maxWords != null) { var words = stringValue.WordCount(); - if (minWords.HasValue && maxWords.HasValue) + if (minWords != null && maxWords != null) { if (minWords == maxWords && minWords != words) { @@ -81,11 +81,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } } - if (minCharacters.HasValue || maxCharacters.HasValue) + if (minCharacters != null || maxCharacters != null) { var characters = stringValue.CharacterCount(); - if (minCharacters.HasValue && maxCharacters.HasValue) + if (minCharacters != null && maxCharacters != null) { if (minCharacters == maxCharacters && minCharacters != characters) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 4410d7c70..04faad2c8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -104,12 +104,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public DomainId AssetId { - get { return Id; } + get => Id; } public DomainId UniqueId { - get { return DocumentId; } + get => DocumentId; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs index 8642ebdba..f65768779 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public DomainId UniqueId { - get { return DocumentId; } + get => DocumentId; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs index 9dc2492f4..eee47176b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs @@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets Filter.Eq(x => x.IsDeleted, false) }; - if (parentId.HasValue) + if (parentId != null) { if (parentId == DomainId.Empty) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs index 2ccbdb7d5..1b96e76eb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return (Map(existing), existing.Version); } - return (null!, EtagVersion.NotFound); + return (null!, EtagVersion.Empty); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index a6b8fb548..285f38de9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return (Map(existing), existing.Version); } - return (null!, EtagVersion.NotFound); + return (null!, EtagVersion.Empty); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index aab6da707..c01e1554d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors Filter.Eq(x => x.IsDeleted, false) }; - if (parentId.HasValue) + if (parentId != null) { if (parentId == DomainId.Empty) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 214c1cddf..a080e3c5d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public DomainId UniqueId { - get { return DocumentId; } + get => DocumentId; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index 5b846304c..98af06066 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -25,6 +25,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents throw new NotSupportedException(); } + Task<(ContentDomainObject.State Value, long Version)> ISnapshotStore.ReadAsync(DomainId key) + { + return Task.FromResult<(ContentDomainObject.State, long Version)>((null!, EtagVersion.Empty)); + } + async Task ISnapshotStore.ClearAsync() { using (Profiler.TraceMethod()) @@ -43,21 +48,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - async Task<(ContentDomainObject.State Value, long Version)> ISnapshotStore.ReadAsync(DomainId key) - { - using (Profiler.TraceMethod()) - { - var contentEntity = await collectionAll.FindAsync(key); - - if (contentEntity != null) - { - return (SimpleMapper.Map(contentEntity, new ContentDomainObject.State()), contentEntity.Version); - } - - return (null!, EtagVersion.NotFound); - } - } - async Task ISnapshotStore.WriteAsync(DomainId key, ContentDomainObject.State value, long oldVersion, long newVersion) { using (Profiler.TraceMethod()) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs index 1f7c5312a..24a675609 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs @@ -114,10 +114,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText } else { - var (bySchema, byApp) = - await AsyncHelper.WhenAll( - SearchBySchemaAsync(queryText, app, filter, scope, LimitHalf), - SearchByAppAsync(queryText, app, scope, LimitHalf)); + var (bySchema, byApp) = await AsyncHelper.WhenAll( + SearchBySchemaAsync(queryText, app, filter, scope, LimitHalf), + SearchByAppAsync(queryText, app, scope, LimitHalf)); return bySchema.Union(byApp).Distinct().ToList(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs index 20744a37d..e29d7543d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs @@ -72,12 +72,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules DomainId IEntity.Id { - get { return DocumentId; } + get => DocumentId; } DomainId IEntity.UniqueId { - get { return DocumentId; } + get => DocumentId; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs index ad04ed92c..89b87d58d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { var filter = Filter.Eq(x => x.AppId, appId); - if (ruleId.HasValue && ruleId.Value != DomainId.Empty) + if (ruleId != null && ruleId.Value != DomainId.Empty) { filter = Filter.And(filter, Filter.Eq(x => x.RuleId, ruleId.Value)); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs index de0867790..9357a2328 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs @@ -24,22 +24,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { public int BatchSize { - get { return 1000; } + get => 1000; } public int BatchDelay { - get { return 500; } + get => 500; } public string Name { - get { return GetType().Name; } + get => GetType().Name; } public string EventsFilter { - get { return "^(app-|schema-)"; } + get => "^(app-|schema-)"; } public MongoSchemasHash(IMongoDatabase database, bool setup = false) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs index f7f9705f7..ddce85477 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands [IgnoreDataMember] public override DomainId AggregateId { - get { return AppId.Id; } + get => AppId.Id; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index dc303a240..98b7bacc4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands [IgnoreDataMember] public override DomainId AggregateId { - get { return AppId; } + get => AppId; } public CreateApp() diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs index 418256aeb..f5a38976d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs @@ -45,14 +45,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await UploadAsync(uploadImage); } - await ExecuteCommandAsync(context); + await base.HandleAsync(context, next); + } - if (context.PlainResult is IAppEntity app) + protected override Task EnrichResultAsync(CommandContext context, CommandResult result) + { + if (result.Payload is IAppEntity app) { contextProvider.Context.App = app; } - await next(context); + return base.EnrichResultAsync(context, result); } private async Task UploadAsync(UploadAppImage uploadImage) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs index cceda46fd..a75bf32a6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject [IgnoreDataMember] public DomainId UniqueId { - get { return Id; } + get => Id; } public override bool ApplyEvent(IEvent @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs index 66fe982c2..23bebc4d2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs @@ -64,12 +64,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return command is AppUpdateCommand update && Equals(update?.AppId?.Id, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { - case CreateApp createApp: - return CreateReturn(createApp, c => + case CreateApp create: + return CreateReturn(create, c => { GuardApp.CanCreate(c); @@ -78,8 +78,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return Snapshot; }); - case UpdateApp updateApp: - return UpdateReturn(updateApp, c => + case UpdateApp update: + return UpdateReturn(update, c => { GuardApp.CanUpdate(c); @@ -304,8 +304,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } }); - case ArchiveApp archiveApp: - return UpdateAsync(archiveApp, async c => + case ArchiveApp archive: + return UpdateAsync(archive, async c => { await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null, null); @@ -322,7 +322,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return appPlansProvider.GetPlanForApp(Snapshot).Plan; } - public void Create(CreateApp command) + private void Create(CreateApp command) { var appId = NamedId.Of(command.AppId, command.Name); @@ -350,7 +350,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } } - public void ChangePlan(ChangePlan command) + private void ChangePlan(ChangePlan command) { if (string.Equals(appPlansProvider.GetFreePlan()?.Id, command.PlanId)) { @@ -362,107 +362,107 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } } - public void Update(UpdateApp command) + private void Update(UpdateApp command) { Raise(command, new AppUpdated()); } - public void UpdateClient(UpdateClient command) + private void UpdateClient(UpdateClient command) { Raise(command, new AppClientUpdated()); } - public void UploadImage(UploadAppImage command) + private void UploadImage(UploadAppImage command) { Raise(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) }); } - public void RemoveImage(RemoveAppImage command) + private void RemoveImage(RemoveAppImage command) { Raise(command, new AppImageRemoved()); } - public void UpdateLanguage(UpdateLanguage command) + private void UpdateLanguage(UpdateLanguage command) { Raise(command, new AppLanguageUpdated()); } - public void AssignContributor(AssignContributor command, bool isAdded) + private void AssignContributor(AssignContributor command, bool isAdded) { Raise(command, new AppContributorAssigned { IsAdded = isAdded }); } - public void RemoveContributor(RemoveContributor command) + private void RemoveContributor(RemoveContributor command) { Raise(command, new AppContributorRemoved()); } - public void AttachClient(AttachClient command) + private void AttachClient(AttachClient command) { Raise(command, new AppClientAttached()); } - public void RevokeClient(RevokeClient command) + private void RevokeClient(RevokeClient command) { Raise(command, new AppClientRevoked()); } - public void AddWorkflow(AddWorkflow command) + private void AddWorkflow(AddWorkflow command) { Raise(command, new AppWorkflowAdded()); } - public void UpdateWorkflow(UpdateWorkflow command) + private void UpdateWorkflow(UpdateWorkflow command) { Raise(command, new AppWorkflowUpdated()); } - public void DeleteWorkflow(DeleteWorkflow command) + private void DeleteWorkflow(DeleteWorkflow command) { Raise(command, new AppWorkflowDeleted()); } - public void AddLanguage(AddLanguage command) + private void AddLanguage(AddLanguage command) { Raise(command, new AppLanguageAdded()); } - public void RemoveLanguage(RemoveLanguage command) + private void RemoveLanguage(RemoveLanguage command) { Raise(command, new AppLanguageRemoved()); } - public void AddPattern(AddPattern command) + private void AddPattern(AddPattern command) { Raise(command, new AppPatternAdded()); } - public void DeletePattern(DeletePattern command) + private void DeletePattern(DeletePattern command) { Raise(command, new AppPatternDeleted()); } - public void UpdatePattern(UpdatePattern command) + private void UpdatePattern(UpdatePattern command) { Raise(command, new AppPatternUpdated()); } - public void AddRole(AddRole command) + private void AddRole(AddRole command) { Raise(command, new AppRoleAdded()); } - public void DeleteRole(DeleteRole command) + private void DeleteRole(DeleteRole command) { Raise(command, new AppRoleDeleted()); } - public void UpdateRole(UpdateRole command) + private void UpdateRole(UpdateRole command) { Raise(command, new AppRoleUpdated()); } - public void ArchiveApp(ArchiveApp command) + private void ArchiveApp(ArchiveApp command) { Raise(command, new AppArchived()); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs index a00e3c61c..b76038828 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation public string Name { - get { return "NotificationEmailSender"; } + get => "NotificationEmailSender"; } public string EventsFilter diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs index de329f969..a809e42ff 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { public bool HasPortal { - get { return false; } + get => false; } public Task ChangePlanAsync(string userId, NamedId appId, string? planId, string? referer) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs index 1a610d0b6..dc457afb7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates .AddField("text", new ContentFieldData() .AddInvariant("Just created a blog with Squidex. I love it!")), - Publish = true + Status = Status.Published }); } @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates .AddField("text", new ContentFieldData() .AddInvariant("I love Squidex and SciFi!")), - Publish = true + Status = Status.Published }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index cca4a2d17..dc1cb9431 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -73,14 +73,12 @@ namespace Squidex.Domain.Apps.Entities.Assets @event.Payload.AssetId, @event.Headers.EventStreamNumber()); - if (asset == null) + if (asset != null) { - throw new DomainObjectNotFoundException(@event.Payload.AssetId.ToString()); - } - - SimpleMapper.Map(asset, result); + SimpleMapper.Map(asset, result); - result.AssetType = asset.Type; + result.AssetType = asset.Type; + } switch (@event.Payload) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDuplicate.cs similarity index 58% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDuplicate.cs index aa932bf36..d239a5c67 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDuplicate.cs @@ -5,19 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetCreatedResult + public sealed record AssetDuplicate(IEnrichedAssetEntity Asset) { - public IEnrichedAssetEntity Asset { get; } - - public bool IsDuplicate { get; } - - public AssetCreatedResult(IEnrichedAssetEntity asset, bool isDuplicate) - { - Asset = asset; - - IsDuplicate = isDuplicate; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs index ae0061f15..5672d8e52 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs @@ -58,12 +58,12 @@ namespace Squidex.Domain.Apps.Entities.Assets public DomainId AssetId { - get { return Id; } + get => Id; } public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs index 22da3f66e..43f26d5b2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs @@ -18,22 +18,22 @@ namespace Squidex.Domain.Apps.Entities.Assets { public int BatchSize { - get { return 1000; } + get => 1000; } public int BatchDelay { - get { return 1000; } + get => 1000; } public string Name { - get { return GetType().Name; } + get => GetType().Name; } public string EventsFilter { - get { return "^asset-"; } + get => "^asset-"; } public Task On(Envelope @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs index f51b114d0..050eb20dc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands [IgnoreDataMember] public DomainId AggregateId { - get { return DomainId.Combine(AppId, AssetId); } + get => DomainId.Combine(AppId, AssetId); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs index 883edfc1c..2b9ed6925 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands [IgnoreDataMember] public DomainId AggregateId { - get { return DomainId.Combine(AppId, AssetFolderId); } + get => DomainId.Combine(AppId, AssetFolderId); } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs similarity index 73% rename from backend/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs index 17a40b462..3cf40c50a 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs @@ -5,10 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public interface IEventEnricher + public enum BulkUpdateAssetType { - void Enrich(Envelope @event, T key); + Annotate, + Move, + Delete } } diff --git a/backend/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs similarity index 59% rename from backend/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs index e40833f21..5319d17c7 100644 --- a/backend/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs @@ -5,15 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public class EntitySavedResult + public sealed class BulkUpdateAssets : SquidexCommand, IAppCommand { - public long Version { get; } + public NamedId AppId { get; set; } - public EntitySavedResult(long version) - { - Version = version; - } + public BulkUpdateJob[]? Jobs { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs new file mode 100644 index 000000000..1c38fb98e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class BulkUpdateJob + { + public BulkUpdateAssetType Type { get; set; } + + public DomainId Id { get; set; } + + public DomainId ParentId { get; set; } + + public string? ParentPath { get; set; } + + public string? FileName { get; set; } + + public string? Slug { get; set; } + + public bool? IsProtected { get; set; } + + public bool Permanent { get; set; } + + public HashSet Tags { get; set; } + + public AssetMetadata? Metadata { get; set; } + + public long ExpectedVersion { get; set; } = EtagVersion.Any; + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 693fdfa27..f88fdd0d7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -5,8 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets.Commands { @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands { public DomainId ParentId { get; set; } - public HashSet Tags { get; set; } = new HashSet(); + public string? ParentPath { get; set; } public bool Duplicate { get; set; } @@ -22,5 +22,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands { AssetId = DomainId.NewGuid(); } + + public MoveAsset AsMove() + { + return SimpleMapper.Map(this, new MoveAsset()); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs index c06b7ac1d..71b98e23c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs @@ -10,5 +10,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public sealed class DeleteAsset : AssetCommand { public bool CheckReferrers { get; set; } + + public bool Permanent { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs index 809b2b422..efdf924db 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs @@ -12,5 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public sealed class MoveAsset : AssetCommand { public DomainId ParentId { get; set; } + + public string? ParentPath { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs index 457d2833b..744a0b35e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using Squidex.Assets; using Squidex.Domain.Apps.Core.Assets; @@ -12,6 +13,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands { public abstract class UploadAssetCommand : AssetCommand { + public HashSet Tags { get; set; } = new HashSet(); + public AssetFile File { get; set; } public AssetMetadata Metadata { get; } = new AssetMetadata(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs new file mode 100644 index 000000000..d093383a8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class UpsertAsset : UploadAssetCommand + { + public DomainId? ParentId { get; set; } + + public string? ParentPath { get; set; } + + public UpsertAsset() + { + AssetId = DomainId.NewGuid(); + } + + public CreateAsset AsCreate() + { + return SimpleMapper.Map(this, new CreateAsset()); + } + + public UpdateAsset AsUpdate() + { + return SimpleMapper.Map(this, new UpdateAsset()); + } + + public MoveAsset AsMove(DomainId parentId) + { + return SimpleMapper.Map(this, new MoveAsset { ParentId = parentId }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs index b65d9d890..fd182a885 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs @@ -19,6 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject public sealed class AssetCommandMiddleware : GrainCommandMiddleware { private readonly IAssetFileStore assetFileStore; + private readonly IAssetFolderResolver assetFolderResolver; private readonly IAssetEnricher assetEnricher; private readonly IAssetQueryService assetQuery; private readonly IContextProvider contextProvider; @@ -28,6 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject IGrainFactory grainFactory, IAssetEnricher assetEnricher, IAssetFileStore assetFileStore, + IAssetFolderResolver assetFolderResolver, IAssetQueryService assetQuery, IContextProvider contextProvider, IEnumerable assetMetadataSources) @@ -35,25 +37,27 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { Guard.NotNull(assetEnricher, nameof(assetEnricher)); Guard.NotNull(assetFileStore, nameof(assetFileStore)); - Guard.NotNull(assetQuery, nameof(assetQuery)); + Guard.NotNull(assetFolderResolver, nameof(assetFolderResolver)); Guard.NotNull(assetMetadataSources, nameof(assetMetadataSources)); + Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(contextProvider, nameof(contextProvider)); - this.assetFileStore = assetFileStore; this.assetEnricher = assetEnricher; - this.assetQuery = assetQuery; + this.assetFileStore = assetFileStore; + this.assetFolderResolver = assetFolderResolver; this.assetMetadataSources = assetMetadataSources; + this.assetQuery = assetQuery; this.contextProvider = contextProvider; } public override async Task HandleAsync(CommandContext context, NextDelegate next) { - var tempFile = context.ContextId.ToString(); - switch (context.Command) { case CreateAsset createAsset: { + var tempFile = context.ContextId.ToString(); + try { await EnrichWithHashAndUploadAsync(createAsset, tempFile); @@ -68,16 +72,25 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject if (existing != null) { - var result = new AssetCreatedResult(existing, true); - - context.Complete(result); + context.Complete(new AssetDuplicate(existing)); await next(context); return; } } - await UploadAsync(context, tempFile, createAsset, createAsset.Tags, true, next); + if (!string.IsNullOrWhiteSpace(createAsset.ParentPath)) + { + createAsset.ParentId = + await assetFolderResolver.ResolveOrCreateAsync( + contextProvider.Context, + context.CommandBus, + createAsset.ParentPath); + } + + await EnrichWithMetadataAsync(createAsset); + + await base.HandleAsync(context, next); } finally { @@ -89,63 +102,97 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject break; } - case UpdateAsset updateAsset: + case MoveAsset move: { - try + if (!string.IsNullOrWhiteSpace(move.ParentPath)) { - await EnrichWithHashAndUploadAsync(updateAsset, tempFile); - - await UploadAsync(context, tempFile, updateAsset, null, false, next); + move.ParentId = + await assetFolderResolver.ResolveOrCreateAsync( + contextProvider.Context, + context.CommandBus, + move.ParentPath); } - finally - { - await assetFileStore.DeleteAsync(tempFile); - updateAsset.File.Dispose(); + await base.HandleAsync(context, next); + + break; + } + + case UpsertAsset upsert: + { + if (!string.IsNullOrWhiteSpace(upsert.ParentPath)) + { + upsert.ParentId = + await assetFolderResolver.ResolveOrCreateAsync( + contextProvider.Context, + context.CommandBus, + upsert.ParentPath); } + await UploadAndHandleAsync(context, next, upsert); + + break; + } + + case UpdateAsset upload: + { + await UploadAndHandleAsync(context, next, upload); + break; } default: - await HandleCoreAsync(context, false, next); + await base.HandleAsync(context, next); break; } } - private async Task UploadAsync(CommandContext context, string tempFile, UploadAssetCommand command, HashSet? tags, bool created, NextDelegate next) + private async Task UploadAndHandleAsync(CommandContext context, NextDelegate next, UploadAssetCommand upload) { - await EnrichWithMetadataAsync(command, tags); + var tempFile = context.ContextId.ToString(); - var asset = await HandleCoreAsync(context, created, next); + try + { + await EnrichWithHashAndUploadAsync(upload, tempFile); + await EnrichWithMetadataAsync(upload); - if (asset != null) + await base.HandleAsync(context, next); + } + finally { - await assetFileStore.CopyAsync(tempFile, command.AppId.Id, command.AssetId, asset.FileVersion); + await assetFileStore.DeleteAsync(tempFile); + + upload.File.Dispose(); } } - private async Task HandleCoreAsync(CommandContext context, bool created, NextDelegate next) + protected override async Task EnrichResultAsync(CommandContext context, CommandResult result) { - await base.HandleAsync(context, next); + var payload = await base.EnrichResultAsync(context, result); - if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity)) + if (payload is IAssetEntity asset) { - var enriched = await assetEnricher.EnrichAsync(asset, contextProvider.Context); - - if (created) + if (result.IsChanged && context.Command is UploadAssetCommand) { - context.Complete(new AssetCreatedResult(enriched, false)); + var tempFile = context.ContextId.ToString(); + + try + { + await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion); + } + catch (AssetAlreadyExistsException) when (context.Command is not UpsertAsset) + { + throw; + } } - else + + if (payload is not IEnrichedAssetEntity) { - context.Complete(enriched); + payload = await assetEnricher.EnrichAsync(asset, contextProvider.Context); } - - return enriched; } - return null; + return payload; } private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) @@ -156,16 +203,18 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { await assetFileStore.UploadAsync(tempFile, hashStream); - command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64(); + var hash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64(); + + command.FileHash = hash; } } } - private async Task EnrichWithMetadataAsync(UploadAssetCommand command, HashSet? tags) + private async Task EnrichWithMetadataAsync(UploadAssetCommand command) { foreach (var metadataSource in assetMetadataSources) { - await metadataSource.EnhanceAsync(command, tags); + await metadataSource.EnhanceAsync(command); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs index 7bfa877d5..fbb0b74e9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs @@ -49,13 +49,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject [IgnoreDataMember] public DomainId AssetId { - get { return Id; } + get => Id; } [IgnoreDataMember] public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } public override bool ApplyEvent(IEvent @event) @@ -68,15 +68,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject SimpleMapper.Map(e, this); - FileName = e.FileName; - - if (string.IsNullOrWhiteSpace(e.Slug)) - { - Slug = e.FileName.ToAssetSlug(); - } - else + if (string.IsNullOrWhiteSpace(Slug)) { - Slug = e.Slug; + Slug = FileName.ToAssetSlug(); } TotalSize += e.FileSize; @@ -86,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject return true; } - case AssetUpdated e: + case AssetUpdated e when Is.Change(e.FileHash, FileHash): { SimpleMapper.Map(e, this); @@ -122,14 +116,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject hasChanged = true; } - if (Is.OptionalChange(Tags, e.Tags)) + if (Is.OptionalSetChange(Tags, e.Tags)) { Tags = e.Tags; hasChanged = true; } - if (Is.OptionalChange(Metadata, e.Metadata)) + if (Is.OptionalMapChange(Metadata, e.Metadata)) { Metadata = e.Metadata; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs index b48b3bf68..f89296618 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs @@ -24,7 +24,7 @@ using IAssetTagService = Squidex.Domain.Apps.Core.Tags.ITagService; namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { - public sealed partial class AssetDomainObject : LogSnapshotDomainObject + public sealed partial class AssetDomainObject : DomainObject { private readonly IContentRepository contentRepository; private readonly IAssetTagService assetTags; @@ -43,6 +43,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject this.assetTags = assetTags; this.assetQuery = assetQuery; this.contentRepository = contentRepository; + + Capacity = int.MaxValue; } protected override bool IsDeleted() @@ -50,6 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject return Snapshot.IsDeleted; } + protected override bool CanRecreate() + { + return true; + } + protected override bool CanAcceptCreation(ICommand command) { return command is AssetCommand; @@ -62,27 +69,45 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Equals(assetCommand.AssetId, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { - case CreateAsset createAsset: - return CreateReturnAsync(createAsset, async c => + case UpsertAsset upsert: + return UpsertReturnAsync(upsert, async c => { - await GuardAsset.CanCreate(c, assetQuery); + if (Version > EtagVersion.Empty && !IsDeleted()) + { + UpdateCore(c.AsUpdate()); + } + else + { + await CreateCore(c.AsCreate()); + } - if (c.Tags != null) + if (Is.OptionalChange(Snapshot.ParentId, c.ParentId)) { - c.Tags = await NormalizeTagsAsync(c.AppId.Id, c.Tags); + await MoveCore(c.AsMove(c.ParentId.Value)); } - Create(c); + return Snapshot; + }); + + case CreateAsset c: + return CreateReturnAsync(c, async create => + { + await CreateCore(create); + + if (Is.Change(Snapshot.ParentId, c.ParentId)) + { + await MoveCore(c.AsMove()); + } return Snapshot; }); - case AnnotateAsset annotateAsset: - return UpdateReturnAsync(annotateAsset, async c => + case AnnotateAsset c: + return UpdateReturnAsync(c, async c => { GuardAsset.CanAnnotate(c); @@ -96,48 +121,81 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject return Snapshot; }); - case UpdateAsset updateAsset: - return UpdateReturn(updateAsset, c => + case UpdateAsset update: + return UpdateReturn(update, update => { - GuardAsset.CanUpdate(c); - - Update(c); + Update(update); return Snapshot; }); - case MoveAsset moveAsset: - return UpdateReturnAsync(moveAsset, async c => + case MoveAsset move: + return UpdateReturnAsync(move, async c => { - await GuardAsset.CanMove(c, Snapshot, assetQuery); - - Move(c); + await MoveCore(c); return Snapshot; }); - case DeleteAsset deleteAsset: - return UpdateAsync(deleteAsset, async c => + case DeleteAsset delete when (delete.Permanent): + return DeletePermanentAsync(delete, async c => { - await GuardAsset.CanDelete(c, Snapshot, contentRepository); - - await assetTags.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags); + await DeleteCore(c); + }); - Delete(c); + case DeleteAsset delete: + return UpdateAsync(delete, async c => + { + await DeleteCore(c); }); default: throw new NotSupportedException(); } } - private async Task> NormalizeTagsAsync(DomainId appId, HashSet tags) + private async Task CreateCore(CreateAsset create) + { + GuardAsset.CanCreate(create); + + if (create.Tags != null) + { + create.Tags = await NormalizeTagsAsync(create.AppId.Id, create.Tags); + } + + Create(create); + } + + private async Task MoveCore(MoveAsset move) + { + await GuardAsset.CanMove(move, Snapshot, assetQuery); + + Move(move); + } + + private void UpdateCore(UpdateAsset update) + { + GuardAsset.CanUpdate(update); + + Update(update); + } + + private async Task DeleteCore(DeleteAsset delete) + { + await GuardAsset.CanDelete(delete, Snapshot, contentRepository); + + await NormalizeTagsAsync(Snapshot.AppId.Id, null); + + Delete(delete); + } + + private async Task> NormalizeTagsAsync(DomainId appId, HashSet? tags) { var normalized = await assetTags.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); return new HashSet(normalized.Values); } - public void Create(CreateAsset command) + private void Create(CreateAsset command) { Raise(command, new AssetCreated { @@ -149,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject }); } - public void Update(UpdateAsset command) + private void Update(UpdateAsset command) { Raise(command, new AssetUpdated { @@ -159,28 +217,24 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject }); } - public void Annotate(AnnotateAsset command) + private void Annotate(AnnotateAsset command) { Raise(command, new AssetAnnotated()); } - public void Move(MoveAsset command) + private void Move(MoveAsset command) { Raise(command, new AssetMoved()); } - public void Delete(DeleteAsset command) + private void Delete(DeleteAsset command) { Raise(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize }); } private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent { - SimpleMapper.Map(command, @event); - - @event.AppId ??= Snapshot.AppId; - - RaiseEvent(Envelope.Create(@event)); + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs index 4ae2cc811..3a34c0070 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { await DomainObject.EnsureLoadedAsync(); - return DomainObject.GetSnapshot(version); + return await DomainObject.GetSnapshotAsync(version); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs index 2967937b5..107a2b513 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject [IgnoreDataMember] public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } public override bool ApplyEvent(IEvent @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs index bb1bd00e3..8a84f269e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs @@ -50,22 +50,22 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Equals(assetFolderCommand.AssetFolderId, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { - case CreateAssetFolder createAssetFolder: - return CreateReturnAsync(createAssetFolder, async c => + case CreateAssetFolder c: + return CreateReturnAsync(c, async create => { - await GuardAssetFolder.CanCreate(c, assetQuery); + await GuardAssetFolder.CanCreate(create, assetQuery); - Create(c); + Create(create); return Snapshot; }); - case MoveAssetFolder moveAssetFolder: - return UpdateReturnAsync(moveAssetFolder, async c => + case MoveAssetFolder move: + return UpdateReturnAsync(move, async c => { await GuardAssetFolder.CanMove(c, Snapshot, assetQuery); @@ -74,8 +74,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject return Snapshot; }); - case RenameAssetFolder renameAssetFolder: - return UpdateReturn(renameAssetFolder, c => + case RenameAssetFolder rename: + return UpdateReturn(rename, c => { GuardAssetFolder.CanRename(c); @@ -84,8 +84,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject return Snapshot; }); - case DeleteAssetFolder deleteAssetFolder: - return Update(deleteAssetFolder, c => + case DeleteAssetFolder delete: + return Update(delete, c => { GuardAssetFolder.CanDelete(c); @@ -97,33 +97,29 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } } - public void Create(CreateAssetFolder command) + private void Create(CreateAssetFolder command) { Raise(command, new AssetFolderCreated()); } - public void Move(MoveAssetFolder command) + private void Move(MoveAssetFolder command) { Raise(command, new AssetFolderMoved()); } - public void Rename(RenameAssetFolder command) + private void Rename(RenameAssetFolder command) { Raise(command, new AssetFolderRenamed()); } - public void Delete(DeleteAssetFolder command) + private void Delete(DeleteAssetFolder command) { Raise(command, new AssetFolderDeleted()); } private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent { - SimpleMapper.Map(command, @event); - - @event.AppId ??= Snapshot.AppId; - - RaiseEvent(Envelope.Create(@event)); + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderResolver.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderResolver.cs new file mode 100644 index 000000000..26ea9e204 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderResolver.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Caching; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +{ + public sealed class AssetFolderResolver : IAssetFolderResolver + { + private static readonly char[] TrimChars = { '/', '\\' }; + private static readonly char[] SplitChars = { ' ', '/', '\\' }; + private readonly ILocalCache localCache; + private readonly IAssetQueryService assetQuery; + + public AssetFolderResolver(ILocalCache localCache, IAssetQueryService assetQuery) + { + Guard.NotNull(localCache, nameof(localCache)); + Guard.NotNull(assetQuery, nameof(assetQuery)); + + this.localCache = localCache; + this.assetQuery = assetQuery; + } + + public async Task ResolveOrCreateAsync(Context context, ICommandBus commandBus, string path) + { + Guard.NotNull(commandBus, nameof(commandBus)); + Guard.NotNull(path, nameof(path)); + + path = path.Trim(TrimChars); + + var elements = path.Split(SplitChars, StringSplitOptions.RemoveEmptyEntries); + + if (elements.Length == 0) + { + return DomainId.Empty; + } + + var currentId = DomainId.Empty; + + var i = elements.Length; + + for (; i > 0; i--) + { + var subPath = string.Join('/', elements.Take(i)); + + if (localCache.TryGetValue(GetCacheKey(subPath), out var cached) && cached is DomainId id) + { + currentId = id; + break; + } + } + + var creating = false; + + for (; i < elements.Length; i++) + { + var name = elements[i]; + + var isResolved = false; + + if (!creating) + { + var children = await assetQuery.QueryAssetFoldersAsync(context, currentId); + + foreach (var child in children) + { + var childPath = string.Join('/', elements.Take(i).Union(Enumerable.Repeat(child.FolderName, 1))); + + localCache.Add(GetCacheKey(childPath), child.Id); + } + + foreach (var child in children) + { + if (child.FolderName == name) + { + currentId = child.Id; + + isResolved = true; + break; + } + } + } + + if (!isResolved) + { + var command = new CreateAssetFolder { ParentId = currentId, FolderName = name }; + + await commandBus.PublishAsync(command); + + currentId = command.AssetFolderId; + creating = true; + } + + var newPath = string.Join('/', elements.Take(i).Union(Enumerable.Repeat(name, 1))); + + localCache.Add(GetCacheKey(newPath), currentId); + } + + return currentId; + } + + private static object GetCacheKey(string path) + { + return $"ASSET_FOLDERS_{path}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs new file mode 100644 index 000000000..c851ab5e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs @@ -0,0 +1,210 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; + +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter +#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it + +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +{ + public sealed class AssetsBulkUpdateCommandMiddleware : ICommandMiddleware + { + private readonly IContextProvider contextProvider; + + private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command) + { + } + + private sealed record BulkTask( + ICommandBus Bus, + int JobIndex, + BulkUpdateJob Job, + BulkUpdateAssets Command, + ConcurrentBag Results + ) + { + } + + public AssetsBulkUpdateCommandMiddleware(IContextProvider contextProvider) + { + Guard.NotNull(contextProvider, nameof(contextProvider)); + + this.contextProvider = contextProvider; + } + + public async Task HandleAsync(CommandContext context, NextDelegate next) + { + if (context.Command is BulkUpdateAssets bulkUpdates) + { + if (bulkUpdates.Jobs?.Length > 0) + { + var executionOptions = new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) + }; + + var createCommandsBlock = new TransformBlock(task => + { + return CreateCommand(task); + }, executionOptions); + + var executeCommandBlock = new ActionBlock(async command => + { + if (command != null) + { + await ExecuteCommandAsync(command); + } + }, executionOptions); + + createCommandsBlock.LinkTo(executeCommandBlock, new DataflowLinkOptions + { + PropagateCompletion = true + }); + + contextProvider.Context.Change(b => b + .WithoutAssetEnrichment() + .WithoutCleanup() + .WithUnpublished(true) + .WithoutTotal()); + + var results = new ConcurrentBag(); + + for (var i = 0; i < bulkUpdates.Jobs.Length; i++) + { + var task = new BulkTask( + context.CommandBus, + i, + bulkUpdates.Jobs[i], + bulkUpdates, + results); + + await createCommandsBlock.SendAsync(task); + } + + createCommandsBlock.Complete(); + + await executeCommandBlock.Completion; + + context.Complete(new BulkUpdateResult(results)); + } + else + { + context.Complete(new BulkUpdateResult()); + } + } + else + { + await next(context); + } + } + + private static async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) + { + var (task, id, command) = bulkCommand; + + Exception? exception = null; + try + { + await task.Bus.PublishAsync(command); + } + catch (Exception ex) + { + exception = ex; + } + + task.Results.Add(new BulkUpdateResultItem + { + Id = id, + JobIndex = task.JobIndex, + Exception = exception + }); + } + + private BulkTaskCommand? CreateCommand(BulkTask task) + { + var id = task.Job.Id; + + try + { + var command = CreateCommandCore(task); + + command.AssetId = id; + + return new BulkTaskCommand(task, id, command); + } + catch (Exception ex) + { + task.Results.Add(new BulkUpdateResultItem + { + Id = id, + JobIndex = task.JobIndex, + Exception = ex + }); + + return null; + } + } + + private AssetCommand CreateCommandCore(BulkTask task) + { + var job = task.Job; + + switch (job.Type) + { + case BulkUpdateAssetType.Annotate: + { + var command = new AnnotateAsset(); + + Enrich(task, command, Permissions.AppAssetsUpdate); + return command; + } + + case BulkUpdateAssetType.Move: + { + var command = new MoveAsset(); + + Enrich(task, command, Permissions.AppAssetsUpdate); + return command; + } + + case BulkUpdateAssetType.Delete: + { + var command = new DeleteAsset(); + + Enrich(task, command, Permissions.AppAssetsDelete); + return command; + } + + default: + throw new NotSupportedException(); + } + } + + private void Enrich(BulkTask task, T command, string permissionId) where T : AssetCommand + { + SimpleMapper.Map(task.Command, command); + SimpleMapper.Map(task.Job, command); + + if (!contextProvider.Context.Allows(permissionId)) + { + throw new DomainForbiddenException("Forbidden"); + } + + command.ExpectedVersion = task.Command.ExpectedVersion; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs index 5b3d387e6..f1b1675af 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs @@ -22,14 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards Guard.NotNull(command, nameof(command)); } - public static Task CanCreate(CreateAsset command, IAssetQueryService assetQuery) + public static void CanCreate(CreateAsset command) { Guard.NotNull(command, nameof(command)); - - return Validate.It(async e => - { - await CheckPathAsync(command.AppId.Id, command.ParentId, assetQuery, e); - }); } public static Task CanMove(MoveAsset command, IAssetEntity asset, IAssetQueryService assetQuery) diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderResolver.cs similarity index 54% rename from backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderResolver.cs index 98d439c91..445458bef 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderResolver.cs @@ -5,16 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { - public class DefaultEventEnricher : IEventEnricher + public interface IAssetFolderResolver { - public virtual void Enrich(Envelope @event, TKey key) - { - if (key is DomainId domainId) - { - @event.SetAggregateId(domainId); - } - } + Task ResolveOrCreateAsync(Context context, ICommandBus commandBus, string path); } -} +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs index 0e6b18c82..7f8408163 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs @@ -28,12 +28,12 @@ namespace Squidex.Domain.Apps.Entities.Assets public string Name { - get { return file.FileName; } + get => file.FileName; } public Stream ReadStream { - get { return file.OpenRead(); } + get => file.OpenRead(); } public Stream WriteStream @@ -52,14 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - public Task EnhanceAsync(UploadAssetCommand command, HashSet? tags) - { - Enhance(command); - - return Task.CompletedTask; - } - - private static void Enhance(UploadAssetCommand command) + public Task EnhanceAsync(UploadAssetCommand command) { try { @@ -67,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { if (file.Properties == null) { - return; + return Task.CompletedTask; } var type = file.Properties.MediaTypes; @@ -146,10 +139,12 @@ namespace Squidex.Domain.Apps.Entities.Assets TryAddString("description", file.Properties.Description); } + + return Task.CompletedTask; } catch { - return; + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs index 92f603a52..bf58ff122 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs @@ -14,15 +14,15 @@ namespace Squidex.Domain.Apps.Entities.Assets { public sealed class FileTypeAssetMetadataSource : IAssetMetadataSource { - public Task EnhanceAsync(UploadAssetCommand command, HashSet? tags) + public Task EnhanceAsync(UploadAssetCommand command) { - if (tags != null) + if (command.Tags != null) { var extension = command.File?.FileName?.FileType(); if (!string.IsNullOrWhiteSpace(extension)) { - tags.Add($"type/{extension.ToLowerInvariant()}"); + command.Tags.Add($"type/{extension.ToLowerInvariant()}"); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs index 8b4c64cf5..5b8a8fc2b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs @@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetMetadataSource { - Task EnhanceAsync(UploadAssetCommand command, HashSet? tags); + Task EnhanceAsync(UploadAssetCommand command); IEnumerable Format(IAssetEntity asset); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index a5c1fd72d..bece97858 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - public async Task EnhanceAsync(UploadAssetCommand command, HashSet? tags) + public async Task EnhanceAsync(UploadAssetCommand command) { if (command.Type == AssetType.Unknown || command.Type == AssetType.Image) { @@ -96,23 +96,23 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - if (command.Type == AssetType.Image && tags != null) + if (command.Type == AssetType.Image && command.Tags != null) { - tags.Add("image"); + command.Tags.Add("image"); var wh = command.Metadata.GetPixelWidth() + command.Metadata.GetPixelWidth(); if (wh > 2000) { - tags.Add("image/large"); + command.Tags.Add("image/large"); } else if (wh > 1000) { - tags.Add("image/medium"); + command.Tags.Add("image/medium"); } else { - tags.Add("image/small"); + command.Tags.Add("image/small"); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index 33ba1f149..30e0777bd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -28,12 +28,12 @@ namespace Squidex.Domain.Apps.Entities.Assets public string Name { - get { return GetType().Name; } + get => GetType().Name; } public string EventsFilter { - get { return "^assetFolder-"; } + get => "^assetFolder-"; } public RecursiveDeleter( diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs index 5f919450d..aa99c5ea0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Backup public RefToken Initiator { - get { return UserMapping.Initiator; } + get => UserMapping.Initiator; } protected BackupContextBase(DomainId appId, IUserMapping userMapping) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs index acd05282d..f075898f0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Backup private async Task RecoverAfterRestartAsync() { - state.Value.Jobs.RemoveAll(x => !x.Stopped.HasValue); + state.Value.Jobs.RemoveAll(x => x.Stopped == null); await state.WriteAsync(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs index c099da64b..5290c985b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -27,12 +27,12 @@ namespace Squidex.Domain.Apps.Entities.Backup public int ReadEvents { - get { return readEvents; } + get => readEvents; } public int ReadAttachments { - get { return readAttachments; } + get => readAttachments; } public BackupReader(IJsonSerializer serializer, Stream stream) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs index a44342981..e4254e079 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs @@ -27,12 +27,12 @@ namespace Squidex.Domain.Apps.Entities.Backup public int WrittenEvents { - get { return writtenEvents; } + get => writtenEvents; } public int WrittenAttachments { - get { return writtenAttachments; } + get => writtenAttachments; } public BackupWriter(IJsonSerializer serializer, Stream stream, bool keepOpen = false, BackupVersion version = BackupVersion.V2) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs index 7d8174982..bed3f1772 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Backup private RestoreJob CurrentJob { - get { return state.Value.Job; } + get => state.Value.Job; } public RestoreGrain( diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs index cff676397..42fb69dae 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Backup public RefToken Initiator { - get { return initiator; } + get => initiator; } public UserMapping(RefToken initiator) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateResult.cs b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResult.cs similarity index 93% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResult.cs index d09faf539..2a75d0693 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResult.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities { public sealed class BulkUpdateResult : List { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateResultItem.cs b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResultItem.cs similarity index 85% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateResultItem.cs rename to backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResultItem.cs index e4cfc7288..efbfd8132 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateResultItem.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResultItem.cs @@ -8,11 +8,11 @@ using System; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities { public sealed class BulkUpdateResultItem { - public DomainId? ContentId { get; set; } + public DomainId? Id { get; set; } public int JobIndex { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsGrain.cs index 9d25e9ae0..1d2190e0c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsGrain.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject private long Version { - get { return version; } + get => version; } public CommentsGrain(IEventStore eventStore, IEventDataFormatter eventDataFormatter) @@ -59,14 +59,14 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject } } - public async Task> ExecuteAsync(J command) + public async Task> ExecuteAsync(J command) { var result = await ExecuteAsync(command.Value); return result.AsJ(); } - private Task ExecuteAsync(CommentsCommand command) + private Task ExecuteAsync(CommentsCommand command) { switch (command) { @@ -76,8 +76,6 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject GuardComments.CanCreate(c); Create(c); - - return EntityCreatedResult.Create(createComment.CommentId, Version); }); case UpdateComment updateComment: @@ -86,8 +84,6 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject GuardComments.CanUpdate(c, Key, events); Update(c); - - return new EntitySavedResult(Version); }); case DeleteComment deleteComment: @@ -96,8 +92,6 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject GuardComments.CanDelete(c, Key, events); Delete(c); - - return new EntitySavedResult(Version); }); default: @@ -105,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject } } - private async Task Upsert(TCommand command, Func handler) where TCommand : CommentsCommand + private async Task Upsert(TCommand command, Action handler) where TCommand : CommentsCommand { Guard.NotNull(command, nameof(command)); Guard.NotNull(handler, nameof(handler)); @@ -115,11 +109,11 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject throw new DomainObjectVersionException(Key, Version, command.ExpectedVersion); } - var prevVersion = version; + var previousVersion = version; try { - var result = handler(command); + handler(command); if (uncommittedEvents.Count > 0) { @@ -127,16 +121,16 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var eventData = uncommittedEvents.Select(x => eventDataFormatter.ToEventData(x, commitId)).ToList(); - await eventStore.AppendAsync(commitId, streamName, prevVersion, eventData); + await eventStore.AppendAsync(commitId, streamName, previousVersion, eventData); } events.AddRange(uncommittedEvents); - return result; + return new CommandResult(DomainId.Create(Key), Version, previousVersion); } catch { - version = prevVersion; + version = previousVersion; throw; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs index ce395707e..fb78f6111 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs @@ -9,13 +9,14 @@ using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Orleans; namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { public interface ICommentsGrain : IGrainWithStringKey { - Task> ExecuteAsync(J command); + Task> ExecuteAsync(J command); Task GetCommentsAsync(long sinceVersion = EtagVersion.Any); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContentType.cs similarity index 93% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContentType.cs index 6e24ddf8c..51127b4d6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContentType.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public enum BulkUpdateType + public enum BulkUpdateContentType { Upsert, ChangeStatus, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs index 724d37eae..a6e0b7134 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs @@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public bool DoNotValidate { get; set; } + public bool DoNotValidateWorkflow { get; set; } + public bool DoNotScript { get; set; } public bool OptimizeValidation { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs index c694d72ad..1e9f621fd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs @@ -19,16 +19,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public DomainId? Id { get; set; } - public Status Status { get; set; } + public Status? Status { get; set; } public Instant? DueTime { get; set; } - public BulkUpdateType Type { get; set; } + public BulkUpdateContentType Type { get; set; } public ContentData? Data { get; set; } public string? Schema { get; set; } + public bool Permanent { get; set; } + public long ExpectedCount { get; set; } = 1; public long ExpectedVersion { get; set; } = EtagVersion.Any; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs index ebd868345..9df3b00f7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -22,5 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public bool CheckReferrers { get; set; } public bool DoNotValidate { get; set; } + + public bool DoNotValidateWorkflow { get; set; } + + public bool OptimizeValidation { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs index 9dde313e5..0b87859c3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands [IgnoreDataMember] public DomainId AggregateId { - get { return DomainId.Combine(AppId, ContentId); } + get => DomainId.Combine(AppId, ContentId); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs index e0c6782a3..cba71c74d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs @@ -11,10 +11,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands { public abstract class ContentDataCommand : ContentCommand { + public ContentData Data { get; set; } + public bool DoNotValidate { get; set; } - public bool OptimizeValidation { get; set; } + public bool DoNotValidateWorkflow { get; set; } - public ContentData Data { get; set; } + public bool OptimizeValidation { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs deleted file mode 100644 index 56ef5dc38..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Contents.Commands -{ - public abstract class ContentUpdateCommand : ContentDataCommand - { - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs index 2fed70e94..4be3376bb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs @@ -5,17 +5,24 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents.Commands { public sealed class CreateContent : ContentDataCommand, ISchemaCommand { - public bool Publish { get; set; } + public Status? Status { get; set; } public CreateContent() { ContentId = DomainId.NewGuid(); } + + public ChangeContentStatus AsChange(Status status) + { + return SimpleMapper.Map(this, new ChangeContentStatus { Status = status }); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs index 4d7c41f5f..a5f245e6f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs @@ -10,5 +10,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public sealed class DeleteContent : ContentCommand { public bool CheckReferrers { get; set; } + + public bool Permanent { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs index 6654339d9..08dece1df 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public sealed class PatchContent : ContentUpdateCommand + public sealed class PatchContent : UpdateContent { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs index aeb2ce59e..40a2c2138 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public sealed class UpdateContent : ContentUpdateCommand + public class UpdateContent : ContentDataCommand { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs index f40ff04b0..5db1f76d0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs @@ -5,17 +5,36 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents.Commands { public sealed class UpsertContent : ContentDataCommand, ISchemaCommand { - public bool Publish { get; set; } + public Status? Status { get; set; } + + public bool CheckReferrers { get; set; } public UpsertContent() { ContentId = DomainId.NewGuid(); } + + public CreateContent AsCreate() + { + return SimpleMapper.Map(this, new CreateContent()); + } + + public UpdateContent AsUpdate() + { + return SimpleMapper.Map(this, new UpdateContent()); + } + + public ChangeContentStatus AsChange(Status status) + { + return SimpleMapper.Map(this, new ChangeContentStatus { Status = status }); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index d50247f41..5ba06184a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -77,13 +77,11 @@ namespace Squidex.Domain.Apps.Entities.Contents @event.Payload.ContentId, @event.Headers.EventStreamNumber()); - if (content == null) + if (content != null) { - throw new DomainObjectNotFoundException(@event.Payload.ContentId.ToString()); + SimpleMapper.Map(content, result); } - SimpleMapper.Map(content, result); - switch (@event.Payload) { case ContentCreated: @@ -115,23 +113,25 @@ namespace Squidex.Domain.Apps.Entities.Contents { result.Type = EnrichedContentEventType.Updated; - var previousContent = - await contentLoader.GetAsync( - content.AppId.Id, - content.Id, - content.Version - 1); - - if (previousContent == null) + if (content != null) { - throw new DomainObjectNotFoundException(@event.Payload.ContentId.ToString()); + var previousContent = + await contentLoader.GetAsync( + content.AppId.Id, + content.Id, + content.Version - 1); + + if (previousContent != null) + { + result.DataOld = previousContent.Data; + } } - result.DataOld = previousContent.Data; break; } } - result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; + result.Name = $"{@event.Payload.SchemaId.Name.ToPascalCase()}{result.Type}"; return result; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 3b87681a5..08f469141 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index aede3db19..ae13c8f6a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -52,9 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Contents return Task.FromResult(Status.Draft); } - public Task CanPublishOnCreateAsync(ISchemaEntity schema, ContentData data, ClaimsPrincipal? user) + public Task CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) { - return Task.FromResult(true); + var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); + + return Task.FromResult(result); } public Task CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) @@ -71,11 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents return Task.FromResult(result); } - public Task GetInfoAsync(IContentEntity content, Status status) + public Task GetInfoAsync(IContentEntity content, Status status) { - var result = Flow[status].Info; + var result = Flow.GetValueOrDefault(status).Info; - return Task.FromResult(result); + return Task.FromResult(result); } public Task GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs index 1c74539d5..ff92a9d39 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs @@ -29,21 +29,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject this.contextProvider = contextProvider; } - public override async Task HandleAsync(CommandContext context, NextDelegate next) + protected override async Task EnrichResultAsync(CommandContext context, CommandResult result) { - await base.HandleAsync(context, next); + var payload = await base.EnrichResultAsync(context, result); - if (context.PlainResult is IContentEntity content && NotEnriched(context)) + if (payload is IContentEntity content && payload is not IEnrichedContentEntity) { - var enriched = await contentEnricher.EnrichAsync(content, true, contextProvider.Context); - - context.Complete(enriched); + payload = await contentEnricher.EnrichAsync(content, true, contextProvider.Context); } - } - private static bool NotEnriched(CommandContext context) - { - return !(context.PlainResult is IEnrichedContentEntity); + return payload; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs index 4d15c390e..2523718c9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs @@ -31,31 +31,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject [IgnoreDataMember] public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } [IgnoreDataMember] public ContentData Data { - get { return NewVersion?.Data ?? CurrentVersion.Data; } + get => NewVersion?.Data ?? CurrentVersion.Data; } [IgnoreDataMember] public Status EditingStatus { - get { return NewStatus ?? Status; } + get => NewStatus ?? Status; } [IgnoreDataMember] public Status Status { - get { return CurrentVersion.Status; } + get => CurrentVersion.Status; } [IgnoreDataMember] public Status? NewStatus { - get { return NewVersion?.Status; } + get => NewVersion?.Status; } public override bool ApplyEvent(IEvent @event, EnvelopeHeaders headers) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index 6d4bda942..c4587692f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -23,7 +23,7 @@ using Squidex.Log; namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { - public sealed partial class ContentDomainObject : LogSnapshotDomainObject + public sealed partial class ContentDomainObject : DomainObject { private readonly ContentOperationContext context; @@ -34,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Guard.NotNull(context, nameof(context)); this.context = context; + + Capacity = int.MaxValue; } protected override bool IsDeleted() @@ -43,7 +45,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject protected override bool CanAcceptCreation(ICommand command) { - return command is CreateContent; + return command is CreateContent || command is UpsertContent; + } + + protected override bool CanRecreate() + { + return true; } protected override bool CanAccept(ICommand command) @@ -54,80 +61,51 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Equals(contentCommand.ContentId, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { - case UpsertContent uspertContent: + case UpsertContent upsertContent: + return UpsertReturnAsync(upsertContent, async c => { - if (Version > EtagVersion.Empty) - { - var updateContent = SimpleMapper.Map(uspertContent, new UpdateContent()); + await LoadContext(c, c.OptimizeValidation); - return ExecuteAsync(updateContent); + if (Version > EtagVersion.Empty && !IsDeleted()) + { + await UpdateCore(c.AsUpdate(), x => c.Data, false); } else { - var createContent = SimpleMapper.Map(uspertContent, new CreateContent()); - - return ExecuteAsync(createContent); + await CreateCore(c.AsCreate()); } - } - - case CreateContent createContent: - return CreateReturnAsync(createContent, async c => - { - await LoadContext(c, c.OptimizeValidation); - await GuardContent.CanCreate(c, context.Workflow, context.Schema); - - var status = await context.GetInitialStatusAsync(); - - if (!c.DoNotValidate) + if (Is.OptionalChange(Snapshot.EditingStatus, c.Status)) { - await context.ValidateInputAsync(c.Data, createContent.Publish); + await ChangeCore(c.AsChange(c.Status.Value)); } - if (!c.DoNotScript) - { - c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Create, - new ScriptVars - { - Operation = "Create", - Data = c.Data, - Status = status, - StatusOld = default - }); - } + return Snapshot; + }); - await context.GenerateDefaultValuesAsync(c.Data); + case CreateContent createContent: + return CreateReturnAsync(createContent, async c => + { + await LoadContext(c, false); - if (!c.DoNotValidate) - { - await context.ValidateContentAsync(c.Data); - } + await CreateCore(c); - if (!c.DoNotScript && c.Publish) + if (Is.OptionalChange(Snapshot.EditingStatus, c.Status)) { - c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Change, - new ScriptVars - { - Operation = "Published", - Data = c.Data, - Status = Status.Published, - StatusOld = default - }); + await ChangeCore(c.AsChange(c.Status.Value)); } - Create(c, status); - return Snapshot; }); - case ValidateContent validateContent: - return UpdateReturnAsync(validateContent, async c => + case ValidateContent validate: + return UpdateReturnAsync(validate, async c => { - await LoadContext(c); + await LoadContext(c, false); GuardContent.CanValidate(c, Snapshot); @@ -136,10 +114,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject return true; }); - case CreateContentDraft createContentDraft: - return UpdateReturnAsync(createContentDraft, async c => + case CreateContentDraft createDraft: + return UpdateReturnAsync(createDraft, async c => { - await LoadContext(c); + await LoadContext(c, false); GuardContent.CanCreateDraft(c, Snapshot); @@ -150,10 +128,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject return Snapshot; }); - case DeleteContentDraft deleteContentDraft: - return UpdateReturnAsync(deleteContentDraft, async c => + case DeleteContentDraft deleteDraft: + return UpdateReturnAsync(deleteDraft, async c => { - await LoadContext(c); + await LoadContext(c, false); GuardContent.CanDeleteDraft(c, Snapshot); @@ -162,20 +140,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject return Snapshot; }); - case UpdateContent updateContent: - return UpdateReturnAsync(updateContent, async c => + case PatchContent patchContent: + return UpdateReturnAsync(patchContent, async c => { - await GuardContent.CanUpdate(c, Snapshot, context.Workflow); + await LoadContext(c, c.OptimizeValidation); - return await UpdateAsync(c, x => c.Data, false); + await UpdateCore(c, c.Data.MergeInto, true); + + return Snapshot; }); - case PatchContent patchContent: - return UpdateReturnAsync(patchContent, async c => + case UpdateContent updateContent: + return UpdateReturnAsync(updateContent, async c => { - await GuardContent.CanPatch(c, Snapshot, context.Workflow); + await LoadContext(c, c.OptimizeValidation); - return await UpdateAsync(c, c.Data.MergeInto, true); + await UpdateCore(c, x => c.Data, false); + + return Snapshot; }); case ChangeContentStatus changeContentStatus: @@ -183,45 +165,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { try { - await LoadContext(c); - - await GuardContent.CanChangeStatus(c, Snapshot, context.Workflow, context.Repository, context.Schema); + await LoadContext(c, c.OptimizeValidation); - if (c.DueTime.HasValue) + if (c.DueTime > SystemClock.Instance.GetCurrentInstant()) { ScheduleStatus(c, c.DueTime.Value); } else { - var change = GetChange(c.Status); - - if (!c.DoNotScript && context.HasScript(c => c.Change)) - { - var data = Snapshot.Data.Clone(); - - var newData = await context.ExecuteScriptAndTransformAsync(s => s.Change, - new ScriptVars - { - Operation = change.ToString(), - Data = data, - Status = c.Status, - StatusOld = Snapshot.EditingStatus - }); - - if (!newData.Equals(Snapshot.Data)) - { - var command = SimpleMapper.Map(c, new UpdateContent { Data = newData }); - - Update(command, newData); - } - } - - if (!c.DoNotValidate && change == StatusChange.Published) - { - await context.ValidateOnPublishAsync(Snapshot.Data); - } - - ChangeStatus(c, change); + await ChangeCore(c); } } catch (Exception) @@ -239,26 +191,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject return Snapshot; }); + case DeleteContent deleteContent when (deleteContent.Permanent): + return DeletePermanentAsync(deleteContent, async c => + { + await DeleteCore(c); + }); + case DeleteContent deleteContent: return UpdateAsync(deleteContent, async c => { - await LoadContext(c); - - await GuardContent.CanDelete(c, Snapshot, context.Repository, context.Schema); - - if (!c.DoNotScript) - { - await context.ExecuteScriptAsync(s => s.Delete, - new ScriptVars - { - Operation = "Delete", - Data = Snapshot.Data, - Status = Snapshot.EditingStatus, - StatusOld = default - }); - } - - Delete(c); + await DeleteCore(c); }); default: @@ -266,107 +208,183 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } } - private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial) + private async Task CreateCore(CreateContent c) { - var currentData = Snapshot.Data; + var status = await context.GetInitialStatusAsync(); + + GuardContent.CanCreate(c, context.Schema); - var newData = newDataFunc(currentData!); + var dataNew = c.Data; - if (!currentData!.Equals(newData)) + if (!c.DoNotValidate) { - await LoadContext(command, command.OptimizeValidation); + await context.ValidateInputAsync(dataNew); + } - if (!command.DoNotValidate) + if (!c.DoNotScript) + { + dataNew = await context.ExecuteScriptAndTransformAsync(s => s.Create, + new ScriptVars + { + Operation = "Create", + Data = dataNew, + Status = status, + StatusOld = default + }); + } + + await context.GenerateDefaultValuesAsync(dataNew); + + if (!c.DoNotValidate) + { + await context.ValidateContentAsync(dataNew); + } + + Create(c, dataNew, status); + } + + private async Task ChangeCore(ChangeContentStatus c) + { + await GuardContent.CanChangeStatus(c, Snapshot, context.Workflow, context.Repository, context.Schema); + + if (c.Status != Snapshot.Status) + { + if (!c.DoNotScript && context.HasScript(c => c.Change)) + { + var change = GetChange(c.Status); + + var data = Snapshot.Data.Clone(); + + var newData = await context.ExecuteScriptAndTransformAsync(s => s.Change, + new ScriptVars + { + Operation = change.ToString(), + Data = data, + Status = c.Status, + StatusOld = Snapshot.EditingStatus + }); + + if (!newData.Equals(Snapshot.Data)) + { + Update(c, newData); + } + } + + if (!c.DoNotValidate && c.Status == Status.Published) + { + await context.ValidateOnPublishAsync(Snapshot.Data); + } + + ChangeStatus(c); + } + } + + private async Task UpdateCore(UpdateContent c, Func newDataFunc, bool partial) + { + await GuardContent.CanUpdate(c, Snapshot, context.Workflow); + + var dataNew = newDataFunc(Snapshot.Data); + + if (!dataNew.Equals(Snapshot.Data)) + { + if (!c.DoNotValidate) { if (partial) { - await context.ValidateInputPartialAsync(command.Data); + await context.ValidateInputPartialAsync(c.Data); } else { - await context.ValidateInputAsync(command.Data, false); + await context.ValidateInputAsync(c.Data); } } - if (!command.DoNotScript) + if (!c.DoNotScript) { - newData = await context.ExecuteScriptAndTransformAsync(s => s.Update, + dataNew = await context.ExecuteScriptAndTransformAsync(s => s.Update, new ScriptVars { - Operation = "Create", - Data = newData, - DataOld = currentData, + Operation = "Update", + Data = dataNew, + DataOld = Snapshot.Data, Status = Snapshot.EditingStatus, StatusOld = default }); } - if (!command.DoNotValidate) + if (!c.DoNotValidate) { - await context.ValidateContentAsync(newData); + await context.ValidateContentAsync(dataNew); } - Update(command, newData); + Update(c, dataNew); } - - return Snapshot; } - public void Create(CreateContent command, Status status) + private async Task DeleteCore(DeleteContent c) { - Raise(command, new ContentCreated { Status = status }); + await LoadContext(c, false); - if (command.Publish) - { - var published = Status.Published; + await GuardContent.CanDelete(c, Snapshot, context.Repository, context.Schema); - Raise(command, new ContentStatusChanged { Status = published, Change = GetChange(published) }); + if (!c.DoNotScript) + { + await context.ExecuteScriptAsync(s => s.Delete, + new ScriptVars + { + Operation = "Delete", + Data = Snapshot.Data, + Status = Snapshot.EditingStatus, + StatusOld = default + }); } + + Delete(c); } - public void CreateDraft(CreateContentDraft command, Status status) + private void Create(CreateContent command, ContentData data, Status status) { - Raise(command, new ContentDraftCreated { Status = status }); + Raise(command, new ContentCreated { Data = data, Status = status }); } - public void Delete(DeleteContent command) + private void Update(ContentCommand command, ContentData data) { - Raise(command, new ContentDeleted()); + Raise(command, new ContentUpdated { Data = data }); } - public void DeleteDraft(DeleteContentDraft command) + private void ChangeStatus(ChangeContentStatus command) { - Raise(command, new ContentDraftDeleted()); + Raise(command, new ContentStatusChanged { Change = GetChange(command.Status) }); } - public void Update(ContentCommand command, ContentData data) + private void CreateDraft(CreateContentDraft command, Status status) { - Raise(command, new ContentUpdated { Data = data }); + Raise(command, new ContentDraftCreated { Status = status }); } - public void ChangeStatus(ChangeContentStatus command, StatusChange change) + private void Delete(DeleteContent command) { - Raise(command, new ContentStatusChanged { Change = change }); + Raise(command, new ContentDeleted()); } - public void CancelChangeStatus(ChangeContentStatus command) + private void DeleteDraft(DeleteContentDraft command) + { + Raise(command, new ContentDraftDeleted()); + } + + private void CancelChangeStatus(ChangeContentStatus command) { Raise(command, new ContentSchedulingCancelled()); } - public void ScheduleStatus(ChangeContentStatus command, Instant dueTime) + private void ScheduleStatus(ChangeContentStatus command, Instant dueTime) { Raise(command, new ContentStatusScheduled { DueTime = dueTime }); } - private void Raise(T command, TEvent @event) where TEvent : SchemaEvent where T : class + private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent { - SimpleMapper.Map(command, @event); - - @event.AppId ??= Snapshot.AppId; - @event.SchemaId ??= Snapshot.SchemaId; - - RaiseEvent(Envelope.Create(@event)); + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); } private StatusChange GetChange(Status status) @@ -385,14 +403,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } } - private Task LoadContext(ContentCommand command, bool optimized = false) - { - return context.LoadAsync(Snapshot.AppId, Snapshot.SchemaId, command, optimized); - } - - private Task LoadContext(CreateContent command, bool optimized = false) + private Task LoadContext(ContentCommand command, bool optimize) { - return context.LoadAsync(command.AppId, command.SchemaId, command, optimized); + return context.LoadAsync(command.AppId, command.SchemaId, command, optimize); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs index 8881522a4..a5b455821 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { await DomainObject.EnsureLoadedAsync(); - return DomainObject.GetSnapshot(version); + return await DomainObject.GetSnapshotAsync(version); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperationContext.cs index acc54f95b..60163b445 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperationContext.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject public ISchemaEntity Schema { - get { return schema; } + get => schema; } public async Task LoadAsync(NamedId appId, NamedId schemaId, ContentCommand command, bool optimized) @@ -118,11 +118,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject return Task.CompletedTask; } - public async Task ValidateInputAsync(ContentData data, bool publish) + public async Task ValidateInputAsync(ContentData data) { var validator = new ContentValidator(Partition(), - validationContext.AsPublishing(publish), validators, log); + validationContext, validators, log); await validator.ValidateInputAsync(data); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs similarity index 76% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs index 6d6ff323f..f4f3802d5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -22,9 +23,9 @@ using Squidex.Shared; #pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable RECS0082 // Parameter has the same name as a member and hides it -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { - public sealed class BulkUpdateCommandMiddleware : ICommandMiddleware + public sealed class ContentsBulkUpdateCommandMiddleware : ICommandMiddleware { private readonly IContentQueryService contentQuery; private readonly IContextProvider contextProvider; @@ -35,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents private sealed record BulkTask( ICommandBus Bus, - Context Context, string Schema, int JobIndex, BulkUpdateJob Job, @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { } - public BulkUpdateCommandMiddleware(IContentQueryService contentQuery, IContextProvider contextProvider) + public ContentsBulkUpdateCommandMiddleware(IContentQueryService contentQuery, IContextProvider contextProvider) { Guard.NotNull(contentQuery, nameof(contentQuery)); Guard.NotNull(contextProvider, nameof(contextProvider)); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents PropagateCompletion = true }); - var requestContext = contextProvider.Context.Clone(b => b + contextProvider.Context.Change(b => b .WithoutContentEnrichment() .WithoutCleanup() .WithUnpublished(true) @@ -94,7 +94,6 @@ namespace Squidex.Domain.Apps.Entities.Contents { var task = new BulkTask( context.CommandBus, - requestContext, requestedSchema, i, bulkUpdates.Jobs[i], @@ -137,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Contents task.Results.Add(new BulkUpdateResultItem { - ContentId = id, + Id = id, JobIndex = task.JobIndex, Exception = exception }); @@ -160,7 +159,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { - var command = await CreateCommandAsync(id, task); + var command = await CreateCommandAsync(task); + + command.ContentId = id; commands.Add(new BulkTaskCommand(task, id, command)); } @@ -168,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { task.Results.Add(new BulkUpdateResultItem { - ContentId = id, + Id = id, JobIndex = task.JobIndex, Exception = ex }); @@ -187,65 +188,65 @@ namespace Squidex.Domain.Apps.Entities.Contents return commands; } - private async Task CreateCommandAsync(DomainId id, BulkTask task) + private async Task CreateCommandAsync(BulkTask task) { var job = task.Job; switch (job.Type) { - case BulkUpdateType.Create: + case BulkUpdateContentType.Create: { - var command = new CreateContent { Data = job.Data! }; + var command = new CreateContent(); - await EnrichAsync(id, task, command, Permissions.AppContentsCreate); + await EnrichAsync(task, command, Permissions.AppContentsCreate); return command; } - case BulkUpdateType.Update: + case BulkUpdateContentType.Update: { - var command = new UpdateContent { Data = job.Data! }; + var command = new UpdateContent(); - await EnrichAsync(id, task, command, Permissions.AppContentsUpdateOwn); + await EnrichAsync(task, command, Permissions.AppContentsUpdateOwn); return command; } - case BulkUpdateType.Upsert: + case BulkUpdateContentType.Upsert: { - var command = new UpsertContent { Data = job.Data! }; + var command = new UpsertContent(); - await EnrichAsync(id, task, command, Permissions.AppContentsUpsert); + await EnrichAsync(task, command, Permissions.AppContentsUpsert); return command; } - case BulkUpdateType.Patch: + case BulkUpdateContentType.Patch: { - var command = new PatchContent { Data = job.Data! }; + var command = new PatchContent(); - await EnrichAsync(id, task, command, Permissions.AppContentsUpdateOwn); + await EnrichAsync(task, command, Permissions.AppContentsUpdateOwn); return command; } - case BulkUpdateType.Validate: + case BulkUpdateContentType.Validate: { var command = new ValidateContent(); - await EnrichAsync(id, task, command, Permissions.AppContentsReadOwn); + await EnrichAsync(task, command, Permissions.AppContentsReadOwn); return command; } - case BulkUpdateType.ChangeStatus: + case BulkUpdateContentType.ChangeStatus: { - var command = new ChangeContentStatus { Status = job.Status, DueTime = job.DueTime }; + var command = new ChangeContentStatus { Status = job.Status ?? Status.Draft }; - await EnrichAsync(id, task, command, Permissions.AppContentsUpdateOwn); + await EnrichAsync(task, command, Permissions.AppContentsChangeStatusOwn); return command; } - case BulkUpdateType.Delete: + case BulkUpdateContentType.Delete: { var command = new DeleteContent(); - await EnrichAsync(id, task, command, Permissions.AppContentsDeleteOwn); + await EnrichAsync(task, command, Permissions.AppContentsDeleteOwn); return command; } @@ -254,20 +255,19 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private async Task EnrichAsync(DomainId id, BulkTask task, TCommand command, string permissionId) where TCommand : ContentCommand + private async Task EnrichAsync(BulkTask task, T command, string permissionId) where T : ContentCommand { SimpleMapper.Map(task.Command, command); - - command.ContentId = id; + SimpleMapper.Map(task.Job, command); if (!string.IsNullOrWhiteSpace(task.Job.Schema)) { - var schema = await contentQuery.GetSchemaOrThrowAsync(task.Context, task.Schema); + var schema = await contentQuery.GetSchemaOrThrowAsync(contextProvider.Context, task.Schema); command.SchemaId = schema.NamedId(); } - if (!task.Context.Allows(permissionId, command.SchemaId.Name)) + if (!contextProvider.Context.Allows(permissionId, command.SchemaId.Name)) { throw new DomainForbiddenException("Forbidden"); } @@ -288,7 +288,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { task.Job.Query.Take = task.Job.ExpectedCount; - var existing = await contentQuery.QueryAsync(task.Context, task.Schema, Q.Empty.WithJsonQuery(task.Job.Query)); + var existing = await contentQuery.QueryAsync(contextProvider.Context, task.Schema, Q.Empty.WithJsonQuery(task.Job.Query)); if (existing.Total > task.Job.ExpectedCount) { @@ -298,7 +298,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return existing.Select(x => x.Id).ToArray(); } - if (task.Job.Type == BulkUpdateType.Create || task.Job.Type == BulkUpdateType.Upsert) + if (task.Job.Type == BulkUpdateContentType.Create || task.Job.Type == BulkUpdateContentType.Upsert) { return new[] { DomainId.NewGuid() }; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/GuardContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/GuardContent.cs index b18b9e960..c963a1119 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/GuardContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/GuardContent.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Security.Claims; +using System.Linq; using System.Threading.Tasks; -using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Repositories; @@ -22,63 +21,50 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards { public static class GuardContent { - public static async Task CanCreate(CreateContent command, IContentWorkflow contentWorkflow, ISchemaEntity schema) + public static void CanCreate(CreateContent command, ISchemaEntity schema) { Guard.NotNull(command, nameof(command)); - if (schema.SchemaDef.IsSingleton && command.ContentId != schema.Id) - { - throw new DomainException(T.Get("contents.singletonNotCreatable")); - } - - if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User)) + if (schema.SchemaDef.IsSingleton) { - throw new DomainException(T.Get("contents.workflowErorPublishing")); + if (command.ContentId != schema.Id) + { + throw new DomainException(T.Get("contents.singletonNotCreatable")); + } } Validate.It(e => { - ValidateData(command, e); + if (command.Data == null) + { + e(Not.Defined(nameof(command.Data)), nameof(command.Data)); + } }); } - public static async Task CanUpdate(UpdateContent command, - IContentEntity content, - IContentWorkflow contentWorkflow) + public static async Task CanUpdate(UpdateContent command, IContentEntity content, IContentWorkflow contentWorkflow) { Guard.NotNull(command, nameof(command)); - CheckPermission(content, command, Permissions.AppContentsUpdate); + CheckPermission(content, command, Permissions.AppContentsUpdate, Permissions.AppContentsUpsert); - Validate.It(e => + if (!command.DoNotValidateWorkflow) { - ValidateData(command, e); - }); - - await ValidateCanUpdate(content, contentWorkflow, command.User); - } - - public static async Task CanPatch(PatchContent command, - IContentEntity content, - IContentWorkflow contentWorkflow) - { - Guard.NotNull(command, nameof(command)); + var status = content.NewStatus ?? content.Status; - CheckPermission(content, command, Permissions.AppContentsUpdate); + if (!await contentWorkflow.CanUpdateAsync(content, status, command.User)) + { + throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); + } + } Validate.It(e => { - ValidateData(command, e); + if (command.Data == null) + { + e(Not.Defined(nameof(command.Data)), nameof(command.Data)); + } }); - - await ValidateCanUpdate(content, contentWorkflow, command.User); - } - - public static void CanValidate(ValidateContent command, IContentEntity content) - { - Guard.NotNull(command, nameof(command)); - - CheckPermission(content, command, Permissions.AppContentsRead); } public static void CanDeleteDraft(DeleteContentDraft command, IContentEntity content) @@ -105,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards } } - public static Task CanChangeStatus(ChangeContentStatus command, + public static async Task CanChangeStatus(ChangeContentStatus command, IContentEntity content, IContentWorkflow contentWorkflow, IContentRepository contentRepository, @@ -113,42 +99,51 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards { Guard.NotNull(command, nameof(command)); - CheckPermission(content, command, Permissions.AppContentsChangeStatus); + CheckPermission(content, command, Permissions.AppContentsChangeStatus, Permissions.AppContentsUpsert); + + var newStatus = command.Status; if (schema.SchemaDef.IsSingleton) { - if (content.NewStatus == null || command.Status != Status.Published) + if (content.NewStatus == null || newStatus != Status.Published) { throw new DomainException(T.Get("contents.singletonNotChangeable")); } - return Task.CompletedTask; + return; } - return Validate.It(async e => + var oldStatus = content.NewStatus ?? content.Status; + + if (oldStatus == Status.Published && command.CheckReferrers) { - var status = content.NewStatus ?? content.Status; + var hasReferrer = await contentRepository.HasReferrersAsync(content.AppId.Id, command.ContentId, SearchScope.Published); - if (!await contentWorkflow.CanMoveToAsync(content, status, command.Status, command.User)) + if (hasReferrer) { - var values = new { oldStatus = status, newStatus = command.Status }; - - e(T.Get("contents.statusTransitionNotAllowed", values), nameof(command.Status)); + throw new DomainException(T.Get("contents.referenced")); } + } - if (content.Status == Status.Published && command.CheckReferrers) + await Validate.It(async e => + { + if (!command.DoNotValidateWorkflow) { - var hasReferrer = await contentRepository.HasReferrersAsync(content.AppId.Id, command.ContentId, SearchScope.Published); - - if (hasReferrer) + if (!await contentWorkflow.CanMoveToAsync(content, oldStatus, newStatus, command.User)) { - throw new DomainException(T.Get("contents.referenced")); + var values = new { oldStatus, newStatus }; + + e(T.Get("contents.statusTransitionNotAllowed", values), "Status"); } } - - if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant()) + else { - e(T.Get("contents.statusSchedulingNotInFuture"), nameof(command.DueTime)); + var info = await contentWorkflow.GetInfoAsync(content, newStatus); + + if (info == null) + { + e(T.Get("contents.statusNotValid"), "Status"); + } } }); } @@ -178,32 +173,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards } } - private static void ValidateData(ContentDataCommand command, AddValidation e) - { - if (command.Data == null) - { - e(Not.Defined(nameof(command.Data)), nameof(command.Data)); - } - } - - private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, ClaimsPrincipal? user) + public static void CanValidate(ValidateContent command, IContentEntity content) { - var status = content.NewStatus ?? content.Status; + Guard.NotNull(command, nameof(command)); - if (!await contentWorkflow.CanUpdateAsync(content, status, user)) - { - throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); - } + CheckPermission(content, command, Permissions.AppContentsRead); } - public static void CheckPermission(IContentEntity content, ContentCommand command, string permission) + public static void CheckPermission(IContentEntity content, ContentCommand command, params string[] permissions) { if (Equals(content.CreatedBy, command.Actor) || command.User == null) { return; } - if (!command.User.Allows(permission, content.AppId.Name, content.SchemaId.Name)) + if (permissions.All(x => !command.User.Allows(x, content.AppId.Name, content.SchemaId.Name))) { throw new DomainForbiddenException(T.Get("common.errorNoPermission")); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs index 5805e783c..88f8c8739 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -38,6 +38,13 @@ namespace Squidex.Domain.Apps.Entities.Contents return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); } + public async Task CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, data, user); + } + public async Task CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) { var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); @@ -64,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return true; } - public async Task GetInfoAsync(IContentEntity content, Status status) + public async Task GetInfoAsync(IContentEntity content, Status status) { var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); @@ -73,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return new StatusInfo(status, GetColor(step)); } - return new StatusInfo(status, StatusColors.Draft); + return null; } public async Task GetInitialStatusAsync(ISchemaEntity schema) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 55ed5ab6e..48c2307ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public IServiceProvider Services { - get { return serviceProvider; } + get => serviceProvider; } public CachingGraphQLService(IBackgroundCache cache, ISchemasHash schemasHash, IServiceProvider serviceProvider, IOptions options) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs index 9dd14512c..24a696d8e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public SharedTypes SharedTypes { - get { return sharedTypes; } + get => sharedTypes; } static Builder() diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index dcde7900c..4b119f218 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -196,6 +196,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents ResolvedType = AllTypes.Boolean }, new QueryArgument(AllTypes.None) + { + Name = "status", + Description = "The initial status.", + DefaultValue = null, + ResolvedType = AllTypes.String + }, + new QueryArgument(AllTypes.None) { Name = "id", Description = "The optional custom content id.", @@ -207,17 +214,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsCreate, c => { - var publish = c.GetArgument("publish"); var contentData = GetContentData(c); var contentId = c.GetArgument("id"); + var contentStatus = c.GetArgument("status"); - var command = new CreateContent { Data = contentData, Publish = publish }; + var command = new CreateContent { Data = contentData }; if (!string.IsNullOrWhiteSpace(contentId)) { - var id = DomainId.Create(contentId); + command.ContentId = DomainId.Create(contentId); + } - command.ContentId = id; + if (!string.IsNullOrWhiteSpace(contentStatus)) + { + command.Status = new Status(contentStatus); + } + else if (c.GetArgument("publish")) + { + command.Status = Status.Published; } return command; @@ -252,6 +266,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents ResolvedType = AllTypes.Boolean }, new QueryArgument(AllTypes.None) + { + Name = "status", + Description = "The initial status.", + DefaultValue = null, + ResolvedType = AllTypes.String + }, + new QueryArgument(AllTypes.None) { Name = "expectedVersion", Description = "The expected version", @@ -263,14 +284,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsUpsert, c => { - var publish = c.GetArgument("publish"); - var contentData = GetContentData(c); var contentId = c.GetArgument("id"); + var contentStatus = c.GetArgument("status"); var id = DomainId.Create(contentId); - return new UpsertContent { ContentId = id, Data = contentData, Publish = publish }; + var command = new UpsertContent { ContentId = id, Data = contentData }; + + if (!string.IsNullOrWhiteSpace(contentStatus)) + { + command.Status = new Status(contentStatus); + } + else if (c.GetArgument("publish")) + { + command.Status = Status.Published; + } + + return command; }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs index fa60fcf80..00e8c8e5d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - internal sealed class EntitySavedGraphType : ObjectGraphType + internal sealed class EntitySavedGraphType : ObjectGraphType { public static readonly IGraphType Nullable = new EntitySavedGraphType(); @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives private static IFieldResolver ResolveVersion() { - return Resolvers.Sync(x => x.Version); + return Resolvers.Sync(x => x.NewVersion); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index cdda08e44..bd9adedce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -16,13 +16,13 @@ namespace Squidex.Domain.Apps.Entities.Contents { Task GetInitialStatusAsync(ISchemaEntity schema); - Task CanPublishOnCreateAsync(ISchemaEntity schema, ContentData data, ClaimsPrincipal? user); + Task CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user); Task CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user); Task CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user); - Task GetInfoAsync(IContentEntity content, Status status); + Task GetInfoAsync(IContentEntity content, Status status); Task GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 8e621f57f..c49b720b1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private IContentQueryService ContentQuery { - get { return contentQuery.Value; } + get => contentQuery.Value; } public ContentEnricher(IEnumerable steps, Lazy contentQuery) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs index 2fcb1b9e8..88f366bd2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps { result.StatusColor = await GetColorAsync(content, content.Status, cache); - if (content.NewStatus.HasValue) + if (content.NewStatus != null) { result.NewStatusColor = await GetColorAsync(content, content.NewStatus.Value, cache); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs index c246032f1..fa229049f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps private IContentQueryService ContentQuery { - get { return contentQuery.Value; } + get => contentQuery.Value; } public ResolveReferences(Lazy contentQuery, IRequestCache requestCache) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs index 6eaa01f6e..9a6d026b6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs @@ -34,8 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents ContentId = contentId, DoNotScript = true, DoNotValidate = true, - Publish = true, - SchemaId = schemaId + SchemaId = schemaId, + Status = Status.Published }; SimpleMapper.Map(createSchema, content); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs index cc8e3a016..f5c358dbe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs @@ -26,27 +26,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text public int BatchSize { - get { return 1000; } + get => 1000; } public int BatchDelay { - get { return 1000; } + get => 1000; } public string Name { - get { return "TextIndexer5"; } + get => "TextIndexer5"; } public string EventsFilter { - get { return "^content-"; } + get => "^content-"; } public ITextIndex TextIndex { - get { return textIndex; } + get => textIndex; } private sealed class Updates diff --git a/backend/src/Squidex.Domain.Apps.Entities/Context.cs b/backend/src/Squidex.Domain.Apps.Entities/Context.cs index 496b8b363..a964fe722 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Context.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Context.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities { private static readonly IReadOnlyDictionary EmptyHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - public IReadOnlyDictionary Headers { get; } + public IReadOnlyDictionary Headers { get; private set; } public ClaimsPermissions UserPermissions { get; } @@ -91,6 +91,13 @@ namespace Squidex.Domain.Apps.Entities return context; } + public Context Update() + { + context.Headers = headers ?? context.Headers; + + return context; + } + public void Remove(string key) { headers ??= new Dictionary(context.Headers, StringComparer.OrdinalIgnoreCase); @@ -104,6 +111,15 @@ namespace Squidex.Domain.Apps.Entities } } + public Context Change(Action action) + { + var builder = new HeaderBuilder(this); + + action(builder); + + return builder.Update(); + } + public Context Clone(Action action) { var builder = new HeaderBuilder(this); diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs index 1874f014f..63d12b99c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.History public IReadOnlyDictionary Texts { - get { return texts; } + get => texts; } protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs index 293d08510..b2b3cc4aa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -24,17 +24,17 @@ namespace Squidex.Domain.Apps.Entities.History public int BatchSize { - get { return 1000; } + get => 1000; } public int BatchDelay { - get { return 1000; } + get => 1000; } public string Name { - get { return GetType().Name; } + get => GetType().Name; } public HistoryService(IHistoryEventRepository repository, IEnumerable creators, NotifoService notifo) diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs index 3318ee52b..33edeae5c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs @@ -20,37 +20,37 @@ namespace Squidex.Domain.Apps.Entities.History public DomainId Id { - get { return item.Id; } + get => item.Id; } public Instant Created { - get { return item.Created; } + get => item.Created; } public RefToken Actor { - get { return item.Actor; } + get => item.Actor; } public long Version { - get { return item.Version; } + get => item.Version; } public string Channel { - get { return item.Channel; } + get => item.Channel; } public string EventType { - get { return item.EventType; } + get => item.EventType; } public string? Message { - get { return message.Value; } + get => message.Value; } public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary texts) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs index bd37c0193..89587f5d5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications { public bool IsActive { - get { return false; } + get => false; } public Task SendInviteAsync(IUser assigner, IUser user, string appName) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs index 94c5c9776..8782554a2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications public bool IsActive { - get { return true; } + get => true; } public NotificationEmailSender( diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs index 6457491aa..488f514ac 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Commands public DomainId AggregateId { - get { return DomainId.Combine(AppId, RuleId); } + get => DomainId.Combine(AppId, RuleId); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs index 03e5e9762..44622a31c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards { var errors = new List(); - if (trigger.NumDays.HasValue && (trigger.NumDays < 1 || trigger.NumDays > 30)) + if (trigger.NumDays != null && (trigger.NumDays < 1 || trigger.NumDays > 30)) { errors.Add(new ValidationError(Not.Between(nameof(trigger.NumDays), 1, 30), nameof(trigger.NumDays))); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs index 183664e38..26a95a1dc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject [IgnoreDataMember] public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } public override bool ApplyEvent(IEvent @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs index 88992f6d7..addbf788d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject ruleCommand.RuleId.Equals(Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { @@ -77,8 +77,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject return Snapshot; }); - case EnableRule enableRule: - return UpdateReturn(enableRule, c => + case EnableRule enable: + return UpdateReturn(enable, c => { GuardRule.CanEnable(c); @@ -87,8 +87,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject return Snapshot; }); - case DisableRule disableRule: - return UpdateReturn(disableRule, c => + case DisableRule disable: + return UpdateReturn(disable, c => { GuardRule.CanDisable(c); @@ -97,10 +97,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject return Snapshot; }); - case DeleteRule deleteRule: - return Update(deleteRule, c => + case DeleteRule delete: + return Update(delete, c => { - GuardRule.CanDelete(deleteRule); + GuardRule.CanDelete(delete); Delete(c); }); @@ -125,38 +125,34 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject await ruleEnqueuer.EnqueueAsync(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); } - public void Create(CreateRule command) + private void Create(CreateRule command) { Raise(command, new RuleCreated()); } - public void Update(UpdateRule command) + private void Update(UpdateRule command) { Raise(command, new RuleUpdated()); } - public void Enable(EnableRule command) + private void Enable(EnableRule command) { Raise(command, new RuleEnabled()); } - public void Disable(DisableRule command) + private void Disable(DisableRule command) { Raise(command, new RuleDisabled()); } - public void Delete(DeleteRule command) + private void Delete(DeleteRule command) { Raise(command, new RuleDeleted()); } private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent { - SimpleMapper.Map(command, @event); - - @event.AppId ??= Snapshot.AppId; - - RaiseEvent(Envelope.Create(@event)); + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index 4b917fb1c..ce84d5868 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -30,21 +30,16 @@ namespace Squidex.Domain.Apps.Entities.Rules this.contextProvider = contextProvider; } - public override async Task HandleAsync(CommandContext context, NextDelegate next) + protected override async Task EnrichResultAsync(CommandContext context, CommandResult result) { - await base.HandleAsync(context, next); + var payload = await base.EnrichResultAsync(context, result); - if (context.PlainResult is IRuleEntity rule && NotEnriched(context)) + if (payload is IRuleEntity rule && payload is not IEnrichedRuleEntity) { - var enriched = await ruleEnricher.EnrichAsync(rule, contextProvider.Context); - - context.Complete(enriched); + payload = await ruleEnricher.EnrichAsync(rule, contextProvider.Context); } - } - private static bool NotEnriched(CommandContext context) - { - return !(context.PlainResult is IEnrichedRuleEntity); + return payload; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs index c969e4ed1..ffdb4826b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs @@ -144,11 +144,11 @@ namespace Squidex.Domain.Apps.Entities.Rules private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) { - if (result != RuleResult.Success && !nextCall.HasValue) + if (result != RuleResult.Success && nextCall == null) { return RuleJobResult.Failed; } - else if (result != RuleResult.Success && nextCall.HasValue) + else if (result != RuleResult.Success && nextCall != null) { return RuleJobResult.Retry; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index 6892df1c2..2509e1156 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public string Name { - get { return GetType().Name; } + get => GetType().Name; } public RuleEnqueuer(IAppProvider appProvider, IMemoryCache cache, ILocalCache localCache, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs index 0616106fa..efd1170e2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs index e6af8bbf5..7261e90b4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs @@ -134,7 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { var job = state.Value; - if (job.RuleId.HasValue && currentJobToken == null) + if (job.RuleId != null && currentJobToken == null) { if (state.Value.FromSnapshots && continues) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index ed977bff2..0c531c6d9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { var from = GetFromDate(today, target.NumDays); - if (!target.Triggered.HasValue || target.Triggered < from) + if (target.Triggered == null || target.Triggered < from) { var costs = await usageTracker.GetMonthCallsAsync(target.AppId.Id.ToString(), today, null); @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking private static DateTime GetFromDate(DateTime today, int? numDays) { - if (numDays.HasValue) + if (numDays != null) { return today.AddDays(-numDays.Value).AddDays(1); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs index ae785eb2f..17b05b27b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands [IgnoreDataMember] public override DomainId AggregateId { - get { return DomainId.Combine(AppId, SchemaId); } + get => DomainId.Combine(AppId, SchemaId); } public CreateSchema() diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs index fbbc82eb8..da6f129f2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands [IgnoreDataMember] public override DomainId AggregateId { - get { return DomainId.Combine(AppId, SchemaId.Id); } + get => DomainId.Combine(AppId, SchemaId.Id); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs index e57228ce6..be8abf16a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards public static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId, bool allowLocked) { - if (parentId.HasValue) + if (parentId != null) { var arrayField = GetArrayFieldOrThrow(schema, parentId.Value, allowLocked); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs index f83a2a6d7..94085550e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards IArrayField? arrayField = null; - if (command.ParentFieldId.HasValue) + if (command.ParentFieldId != null) { arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs index 01049dbe7..4d14dd881 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards errors.Foreach((x, _) => x.WithPrefix(nameof(command.Properties)).AddTo(e)); } - if (command.ParentFieldId.HasValue) + if (command.ParentFieldId != null) { var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs index 8618ae193..2a3199ba2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject [IgnoreDataMember] public DomainId UniqueId { - get { return DomainId.Combine(AppId, Id); } + get => DomainId.Combine(AppId, Id); } public override bool ApplyEvent(IEvent @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs index 80e9f283b..4f801342e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Equals(schemaCommand.SchemaId?.Id, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject { GuardSchemaField.CanAdd(c, Snapshot.SchemaDef); - Add(c); + AddField(c); return Snapshot; }); @@ -71,8 +71,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject return Snapshot; }); - case SynchronizeSchema synchronizeSchema: - return UpdateReturn(synchronizeSchema, c => + case SynchronizeSchema synchronize: + return UpdateReturn(synchronize, c => { GuardSchema.CanSynchronize(c); @@ -161,8 +161,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject return Snapshot; }); - case UpdateSchema updateSchema: - return UpdateReturn(updateSchema, c => + case UpdateSchema update: + return UpdateReturn(update, c => { GuardSchema.CanUpdate(c); @@ -171,8 +171,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject return Snapshot; }); - case PublishSchema publishSchema: - return UpdateReturn(publishSchema, c => + case PublishSchema publish: + return UpdateReturn(publish, c => { GuardSchema.CanPublish(c); @@ -181,8 +181,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject return Snapshot; }); - case UnpublishSchema unpublishSchema: - return UpdateReturn(unpublishSchema, c => + case UnpublishSchema unpublish: + return UpdateReturn(unpublish, c => { GuardSchema.CanUnpublish(c); @@ -254,7 +254,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject } } - public void Synchronize(SynchronizeSchema command) + private void Synchronize(SynchronizeSchema command) { var options = new SchemaSynchronizationOptions { @@ -273,97 +273,97 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject } } - public void Create(CreateSchema command) + private void Create(CreateSchema command) { Raise(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.BuildSchema() }); } - public void Add(AddField command) + private void AddField(AddField command) { Raise(command, new FieldAdded { FieldId = CreateFieldId(command) }); } - public void UpdateField(UpdateField command) + private void UpdateField(UpdateField command) { Raise(command, new FieldUpdated()); } - public void LockField(LockField command) + private void LockField(LockField command) { Raise(command, new FieldLocked()); } - public void HideField(HideField command) + private void HideField(HideField command) { Raise(command, new FieldHidden()); } - public void ShowField(ShowField command) + private void ShowField(ShowField command) { Raise(command, new FieldShown()); } - public void DisableField(DisableField command) + private void DisableField(DisableField command) { Raise(command, new FieldDisabled()); } - public void EnableField(EnableField command) + private void EnableField(EnableField command) { Raise(command, new FieldEnabled()); } - public void DeleteField(DeleteField command) + private void DeleteField(DeleteField command) { Raise(command, new FieldDeleted()); } - public void Reorder(ReorderFields command) + private void Reorder(ReorderFields command) { Raise(command, new SchemaFieldsReordered()); } - public void Publish(PublishSchema command) + private void Publish(PublishSchema command) { Raise(command, new SchemaPublished()); } - public void Unpublish(UnpublishSchema command) + private void Unpublish(UnpublishSchema command) { Raise(command, new SchemaUnpublished()); } - public void ConfigureScripts(ConfigureScripts command) + private void ConfigureScripts(ConfigureScripts command) { Raise(command, new SchemaScriptsConfigured()); } - public void ConfigureFieldRules(ConfigureFieldRules command) + private void ConfigureFieldRules(ConfigureFieldRules command) { Raise(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() }); } - public void ChangeCategory(ChangeCategory command) + private void ChangeCategory(ChangeCategory command) { Raise(command, new SchemaCategoryChanged()); } - public void ConfigurePreviewUrls(ConfigurePreviewUrls command) + private void ConfigurePreviewUrls(ConfigurePreviewUrls command) { Raise(command, new SchemaPreviewUrlsConfigured()); } - public void ConfigureUIFields(ConfigureUIFields command) + private void ConfigureUIFields(ConfigureUIFields command) { Raise(command, new SchemaUIFieldsConfigured()); } - public void Update(UpdateSchema command) + private void Update(UpdateSchema command) { Raise(command, new SchemaUpdated()); } - public void Delete(DeleteSchema command) + private void Delete(DeleteSchema command) { Raise(command, new SchemaDeleted()); } @@ -374,7 +374,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject NamedId? GetFieldId(long? id) { - if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) + if (id != null && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) { return field.NamedId(); } @@ -384,7 +384,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject if (command is ParentFieldCommand parentField && @event is ParentFieldEvent parentFieldEvent) { - if (parentField.ParentFieldId.HasValue) + if (parentField.ParentFieldId != null) { if (Snapshot.SchemaDef.FieldsById.TryGetValue(parentField.ParentFieldId.Value, out var field)) { diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs index 4cc12961c..55cdc1c17 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -174,7 +174,7 @@ namespace Squidex.Domain.Users.MongoDb public IQueryable Users { - get { return Collection.AsQueryable(); } + get => Collection.AsQueryable(); } public bool IsId(string id) diff --git a/backend/src/Squidex.Domain.Users/UserWithClaims.cs b/backend/src/Squidex.Domain.Users/UserWithClaims.cs index b376b1c11..4e7dc2a88 100644 --- a/backend/src/Squidex.Domain.Users/UserWithClaims.cs +++ b/backend/src/Squidex.Domain.Users/UserWithClaims.cs @@ -19,17 +19,17 @@ namespace Squidex.Domain.Users public string Id { - get { return Identity.Id; } + get => Identity.Id; } public string Email { - get { return Identity.Email; } + get => Identity.Email; } public bool IsLocked { - get { return Identity.LockoutEnd > DateTime.UtcNow; } + get => Identity.LockoutEnd > DateTime.UtcNow; } public IReadOnlyList Claims { get; } diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs index 58307d555..95c8fc632 100644 --- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs @@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.EventSourcing public Uri ServiceUri { - get { return documentClient.ServiceEndpoint; } + get => documentClient.ServiceEndpoint; } public CosmosDbEventStore(DocumentClient documentClient, string masterKey, string database, IJsonSerializer jsonSerializer) diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs index dfcf9a848..82abc7f2b 100644 --- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing public bool IsEndOfCommit { - get { return CommitOffset == CommitSize - 1; } + get => CommitOffset == CommitSize - 1; } public StreamPosition(long timestamp, long commitOffset, long commitSize) diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 6b4f34b2b..314c12a17 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -25,12 +25,12 @@ namespace Squidex.Infrastructure.EventSourcing public IMongoCollection RawCollection { - get { return Database.GetCollection(CollectionName()); } + get => Database.GetCollection(CollectionName()); } public IMongoCollection TypedCollection { - get { return Collection; } + get => Collection; } public bool CanUseChangeStreams { get; private set; } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs index 475763e3c..465d6ef4d 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.MongoDb public bool IsDiscriminatorCompatibleWithObjectSerializer { - get { return true; } + get => true; } public BsonType Representation { get; } = BsonType.String; diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs index 63c871406..8b305a0f3 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.MongoDb public bool IsDiscriminatorCompatibleWithObjectSerializer { - get { return true; } + get => true; } public override Instant Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 0853cf91d..691b8192a 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -48,7 +48,7 @@ namespace Squidex.Infrastructure.MongoDb protected IMongoDatabase Database { - get { return mongoDatabase; } + get => mongoDatabase; } static MongoRepositoryBase() diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index b35645180..138bc304e 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -55,7 +55,7 @@ namespace Squidex.Infrastructure.States return (existing.Doc, existing.Version); } - return (default!, EtagVersion.NotFound); + return (default!, EtagVersion.Empty); } } diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs index c10061707..1a91d8a4e 100644 --- a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -29,12 +29,12 @@ namespace Squidex.Infrastructure.CQRS.Events public string Name { - get { return eventPublisherName; } + get => eventPublisherName; } public string EventsFilter { - get { return eventsFilter; } + get => eventsFilter; } public RabbitMqEventConsumer(IJsonSerializer jsonSerializer, string eventPublisherName, string uri, string exchange, string eventsFilter) diff --git a/backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs index 767916111..9d13c8136 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs @@ -31,17 +31,17 @@ namespace Squidex.Infrastructure.Collections public IEnumerable Keys { - get { return inner.Keys; } + get => inner.Keys; } public IEnumerable Values { - get { return inner.Values; } + get => inner.Values; } public int Count { - get { return inner.Count; } + get => inner.Count; } public ImmutableDictionary() diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs index a1dc5f760..6c3d1535b 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs @@ -5,28 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; - namespace Squidex.Infrastructure.Commands { public sealed class CommandContext { - private Tuple? result; - public DomainId ContextId { get; } = DomainId.NewGuid(); public ICommand Command { get; } public ICommandBus CommandBus { get; } - public object? PlainResult - { - get { return result?.Item1; } - } + public object? PlainResult { get; private set; } public bool IsCompleted { - get { return result != null; } + get => PlainResult != null; } public CommandContext(ICommand command, ICommandBus commandBus) @@ -40,14 +33,14 @@ namespace Squidex.Infrastructure.Commands public CommandContext Complete(object? resultValue = null) { - result = Tuple.Create(resultValue); + PlainResult = resultValue ?? None.Value; return this; } public T Result() { - return (T)result?.Item1!; + return (T)PlainResult!; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs b/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs index 7c4410af6..bd10fc067 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs @@ -7,24 +7,12 @@ using System.Globalization; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Infrastructure.Commands { - public sealed class CommandRequest + public sealed record CommandRequest(IAggregateCommand Command, string Culture, string CultureUI) { - public IAggregateCommand Command { get; } - - public string Culture { get; } - - public string CultureUI { get; } - - public CommandRequest(IAggregateCommand command, string culture, string cultureUI) - { - Command = command; - - Culture = culture; - CultureUI = cultureUI; - } - public static CommandRequest Create(IAggregateCommand command) { return new CommandRequest(command, diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandResult.cs b/backend/src/Squidex.Infrastructure/Commands/CommandResult.cs new file mode 100644 index 000000000..8ff5f951a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/CommandResult.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + +namespace Squidex.Infrastructure.Commands +{ + public record CommandResult(DomainId Id, long NewVersion, long OldVersion, object Payload) + { + public bool IsCreated => OldVersion < 0; + + public bool IsChanged => OldVersion != NewVersion; + + public CommandResult(DomainId id, long newVersion, long oldVersion) + : this(id, newVersion, oldVersion, None.Value) + { + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs new file mode 100644 index 000000000..f0e166f06 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs @@ -0,0 +1,261 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public partial class DomainObject + { + protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : ICommand + { + EnsureCanCreate(command); + + return UpsertCoreAsync(command, handler, true); + } + + protected Task CreateReturn(TCommand command, Func handler) where TCommand : ICommand + { + return CreateReturnAsync(command, x => + { + var result = handler(x); + + return Task.FromResult(result); + }); + } + + protected Task CreateAsync(TCommand command, Func handler) where TCommand : ICommand + { + EnsureCanCreate(command); + + return UpsertCoreAsync(command, async x => + { + await handler(x); + + return None.Value; + }, true); + } + + protected Task Create(TCommand command, Action handler) where TCommand : ICommand + { + return CreateAsync(command, x => + { + handler(x); + + return Task.FromResult(None.Value); + }); + } + + protected async Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : ICommand + { + await EnsureCanUpdateAsync(command); + + return await UpsertCoreAsync(command, handler); + } + + protected Task UpdateReturn(TCommand command, Func handler) where TCommand : ICommand + { + return UpdateReturnAsync(command, x => + { + var result = handler(x); + + return Task.FromResult(result); + }); + } + + protected async Task UpdateAsync(TCommand command, Func handler) where TCommand : ICommand + { + await EnsureCanUpdateAsync(command); + + return await UpsertCoreAsync(command, async x => + { + await handler(x); + + return None.Value; + }, true); + } + + protected async Task Update(TCommand command, Action handler) where TCommand : ICommand + { + return await UpdateAsync(command, x => + { + handler(x); + + return Task.FromResult(None.Value); + }); + } + + protected async Task UpsertReturnAsync(TCommand command, Func> handler) where TCommand : ICommand + { + await EnsureCanUpsertAsync(command); + + return await UpsertCoreAsync(command, handler, true); + } + + protected async Task UpsertReturn(TCommand command, Func handler) where TCommand : ICommand + { + return await UpsertReturnAsync(command, x => + { + var result = handler(x); + + return Task.FromResult(result); + }); + } + + protected async Task UpsertAsync(TCommand command, Func handler) where TCommand : ICommand + { + await EnsureCanUpsertAsync(command); + + return await UpsertCoreAsync(command, async x => + { + await handler(x); + + return None.Value; + }, true); + } + + protected async Task Upsert(TCommand command, Action handler) where TCommand : ICommand + { + Guard.NotNull(handler, nameof(handler)); + + return await UpsertAsync(command, x => + { + handler(x); + + return Task.FromResult(None.Value); + }); + } + + protected async Task DeletePermanentAsync(TCommand command, Func handler) where TCommand : ICommand + { + Guard.NotNull(handler, nameof(handler)); + + await EnsureCanDeleteAsync(command); + + return await DeleteCoreAsync(command, async x => + { + await handler(x); + + return None.Value; + }); + } + + protected async Task DeletePermanent(TCommand command, Action handler) where TCommand : ICommand + { + Guard.NotNull(handler, nameof(handler)); + + return await DeletePermanentAsync(command, x => + { + handler(x); + + return Task.FromResult(None.Value); + }); + } + + private void EnsureCanCreate(TCommand command) where TCommand : ICommand + { + Guard.NotNull(command, nameof(command)); + + MatchingVersion(command); + MatchingCreateCommand(command); + + if (Version != EtagVersion.Empty && !(IsDeleted() && CanRecreate())) + { + throw new DomainObjectConflictException(uniqueId.ToString()); + } + } + + private async Task EnsureCanUpdateAsync(TCommand command) where TCommand : ICommand + { + Guard.NotNull(command, nameof(command)); + + await EnsureLoadedAsync(); + + MatchingVersion(command); + MatchingCommand(command); + + NotDeleted(); + NotEmpty(); + } + + private async Task EnsureCanUpsertAsync(TCommand command) where TCommand : ICommand + { + Guard.NotNull(command, nameof(command)); + + await EnsureLoadedAsync(); + + MatchingVersion(command); + + if (Version <= EtagVersion.Empty) + { + MatchingCreateCommand(command); + } + else + { + MatchingCommand(command); + } + + if (IsDeleted() && !CanRecreate()) + { + throw new DomainObjectDeletedException(uniqueId.ToString()); + } + } + + private async Task EnsureCanDeleteAsync(TCommand command) where TCommand : ICommand + { + Guard.NotNull(command, nameof(command)); + + await EnsureLoadedAsync(); + + MatchingVersion(command); + MatchingCommand(command); + + NotEmpty(); + } + + private void NotDeleted() + { + if (IsDeleted()) + { + throw new DomainObjectDeletedException(uniqueId.ToString()); + } + } + + private void NotEmpty() + { + if (Version <= EtagVersion.Empty) + { + throw new DomainObjectNotFoundException(uniqueId.ToString()); + } + } + + private void MatchingVersion(TCommand command) where TCommand : ICommand + { + if (Version > EtagVersion.Empty && command.ExpectedVersion > EtagVersion.Any && Version != command.ExpectedVersion) + { + throw new DomainObjectVersionException(uniqueId.ToString(), Version, command.ExpectedVersion); + } + } + + private void MatchingCreateCommand(TCommand command) where TCommand : ICommand + { + if (!CanAcceptCreation(command)) + { + throw new DomainException("Invalid command."); + } + } + + private void MatchingCommand(TCommand command) where TCommand : ICommand + { + if (!CanAccept(command)) + { + throw new DomainException("Invalid command."); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs index 1529ec0ea..ccb6a7786 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; @@ -12,67 +14,298 @@ using Squidex.Log; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObject : DomainObjectBase where T : class, IDomainState, new() + public abstract partial class DomainObject where T : class, IDomainState, new() { + private readonly List> uncomittedEvents = new List>(); + private readonly SnapshotList snapshots = new SnapshotList(); private readonly IStore store; - private T snapshot = new T { Version = EtagVersion.Empty }; + private readonly ISemanticLog log; private IPersistence? persistence; + private bool isLoaded; + private DomainId uniqueId; - public override T Snapshot + public DomainId UniqueId { - get { return snapshot; } + get => uniqueId; + } + + public long Version + { + get => snapshots.Version; + } + + public T Snapshot + { + get => snapshots.Current; + } + + protected int Capacity + { + get => snapshots.Capacity; + set => snapshots.Capacity = value; } protected DomainObject(IStore store, ISemanticLog log) - : base(log) { Guard.NotNull(store, nameof(store)); + Guard.NotNull(log, nameof(log)); this.store = store; + + this.log = log; } - protected override void OnSetup() + public async Task GetSnapshotAsync(long version) { - persistence = store.WithSnapshotsAndEventSourcing(GetType(), UniqueId, new HandleSnapshot(ApplySnapshot), x => ApplyEvent(x, true)); + var (result, valid) = snapshots.Get(version); + + if (result == null && valid) + { + var snapshot = new T + { + Version = EtagVersion.Empty + }; + + snapshots.Add(snapshot, snapshot.Version, false); + + var allEvents = store.WithEventSourcing(GetType(), UniqueId, @event => + { + var newVersion = snapshot.Version + 1; + + if (!snapshots.Contains(newVersion)) + { + snapshot = Apply(snapshot, @event); + snapshot.Version = newVersion; + + snapshots.Add(snapshot, snapshot.Version, false); + + return true; + } + + return false; + }); + + await allEvents.ReadAsync(); + + (result, valid) = snapshots.Get(version); + } + + return result ?? new T { Version = EtagVersion.Empty }; } - protected sealed override bool ApplyEvent(Envelope @event, bool isLoading) + public virtual void Setup(DomainId uniqueId) { - var newVersion = Version + 1; + this.uniqueId = uniqueId; - var newSnapshot = OnEvent(@event); + persistence = store.WithSnapshotsAndEventSourcing(GetType(), UniqueId, + new HandleSnapshot(snapshot => + { + snapshot.Version = Version + 1; + snapshots.Add(snapshot, snapshot.Version, true); + }), + @event => ApplyEvent(@event, true)); + } - if (!ReferenceEquals(Snapshot, newSnapshot) || isLoading) + public virtual async Task EnsureLoadedAsync(bool silent = false) + { + if (isLoaded) { - snapshot = newSnapshot; - snapshot.Version = newVersion; + return; + } - return true; + if (silent) + { + await ReadAsync(); + } + else + { + var logContext = (id: uniqueId.ToString(), name: GetType().Name); + + using (log.MeasureInformation(logContext, (ctx, w) => w + .WriteProperty("action", "ActivateDomainObject") + .WriteProperty("domainObjectType", ctx.name) + .WriteProperty("domainObjectKey", ctx.id))) + { + await ReadAsync(); + } + } + + isLoaded = true; + } + + protected void RaiseEvent(IEvent @event) + { + RaiseEvent(Envelope.Create(@event)); + } + + protected virtual void RaiseEvent(Envelope @event) + { + Guard.NotNull(@event, nameof(@event)); + + @event.SetAggregateId(uniqueId); + + if (ApplyEvent(@event, false)) + { + uncomittedEvents.Add(@event); + } + } + + public IReadOnlyList> GetUncomittedEvents() + { + return uncomittedEvents; + } + + public void ClearUncommittedEvents() + { + uncomittedEvents.Clear(); + } + + private async Task DeleteCoreAsync(TCommand command, Func> handler) where TCommand : ICommand + { + Guard.NotNull(handler, nameof(handler)); + + var previousSnapshot = Snapshot; + var previousVersion = Version; + try + { + var result = (await handler(command)) ?? None.Value; + + var events = uncomittedEvents.ToArray(); + + if (events != null) + { + var deletedId = DomainId.Combine(UniqueId, DomainId.Create("deleted")); + var deletedStream = store.WithEventSourcing(GetType(), deletedId, null); + + await deletedStream.WriteEventsAsync(events); + + if (persistence != null) + { + await persistence.DeleteAsync(); + } + + snapshots.Clear(); + } + + return new CommandResult(UniqueId, Version, previousVersion, result); + } + catch + { + RestorePreviousSnapshot(previousSnapshot, previousVersion); + + throw; + } + finally + { + ClearUncommittedEvents(); + } + } + + private async Task UpsertCoreAsync(TCommand command, Func> handler, bool isCreation = false) where TCommand : ICommand + { + Guard.NotNull(handler, nameof(handler)); + + var previousSnapshot = Snapshot; + var previousVersion = Version; + try + { + var result = (await handler(command)) ?? None.Value; + + var events = uncomittedEvents.ToArray(); + + try + { + await WriteAsync(events); + } + catch (InconsistentStateException) + { + await EnsureLoadedAsync(true); + + if (IsDeleted()) + { + if (CanRecreate() && isCreation) + { + snapshots.ResetTo(new T(), previousVersion); + + foreach (var @event in uncomittedEvents) + { + ApplyEvent(@event, false); + } + + await WriteAsync(events); + } + else + { + throw new DomainObjectDeletedException(uniqueId.ToString()); + } + } + else + { + RestorePreviousSnapshot(previousSnapshot, previousVersion); + + throw new DomainObjectConflictException(uniqueId.ToString()); + } + } + + isLoaded = true; + + return new CommandResult(UniqueId, Version, previousVersion, result); + } + catch + { + RestorePreviousSnapshot(previousSnapshot, previousVersion); + + throw; + } + finally + { + ClearUncommittedEvents(); } + } + + protected virtual bool CanAcceptCreation(ICommand command) + { + return true; + } + + protected virtual bool CanAccept(ICommand command) + { + return true; + } + protected virtual bool IsDeleted() + { return false; } - protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) + protected virtual bool CanRecreate() { - snapshot = previousSnapshot; + return false; } - private void ApplySnapshot(T state) + private void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) { - snapshot = state; + snapshots.ResetTo(previousSnapshot, previousVersion); } - protected sealed override async Task WriteAsync(Envelope[] newEvents, long previousVersion) + private bool ApplyEvent(Envelope @event, bool isLoading) { - if (newEvents.Length > 0 && persistence != null) + var newVersion = Version + 1; + + var snapshotNew = Apply(Snapshot, @event); + + if (!ReferenceEquals(Snapshot, snapshotNew) || isLoading) { - await persistence.WriteEventsAsync(newEvents); - await persistence.WriteSnapshotAsync(Snapshot); + snapshotNew.Version = newVersion; + snapshots.Add(snapshotNew, snapshotNew.Version, true); + + return true; } + + return false; } - protected sealed override async Task ReadAsync() + private async Task ReadAsync() { if (persistence != null) { @@ -80,7 +313,16 @@ namespace Squidex.Infrastructure.Commands } } - public sealed override async Task RebuildStateAsync() + private async Task WriteAsync(Envelope[] newEvents) + { + if (newEvents.Length > 0 && persistence != null) + { + await persistence.WriteEventsAsync(newEvents); + await persistence.WriteSnapshotAsync(Snapshot); + } + } + + public async Task RebuildStateAsync() { await EnsureLoadedAsync(true); @@ -95,9 +337,11 @@ namespace Squidex.Infrastructure.Commands } } - protected T OnEvent(Envelope @event) + protected virtual T Apply(T snapshot, Envelope @event) { - return Snapshot.Apply(@event); + return snapshot.Apply(@event); } + + public abstract Task ExecuteAsync(IAggregateCommand command); } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs deleted file mode 100644 index 198fcb37e..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ /dev/null @@ -1,267 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; -using Squidex.Log; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class DomainObjectBase where T : IDomainState, new() - { - private readonly List> uncomittedEvents = new List>(); - private readonly ISemanticLog log; - private bool isLoaded; - private DomainId uniqueId; - - public DomainId UniqueId - { - get { return uniqueId; } - } - - public long Version - { - get { return Snapshot.Version; } - } - - public abstract T Snapshot { get; } - - protected DomainObjectBase(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - public virtual void Setup(DomainId uniqueId) - { - this.uniqueId = uniqueId; - - OnSetup(); - } - - public virtual async Task EnsureLoadedAsync(bool silent = false) - { - if (isLoaded) - { - return; - } - - if (silent) - { - await ReadAsync(); - } - else - { - var logContext = (id: uniqueId.ToString(), name: GetType().Name); - - using (log.MeasureInformation(logContext, (ctx, w) => w - .WriteProperty("action", "ActivateDomainObject") - .WriteProperty("domainObjectType", ctx.name) - .WriteProperty("domainObjectKey", ctx.id))) - { - await ReadAsync(); - } - } - - isLoaded = true; - } - - protected void RaiseEvent(IEvent @event) - { - RaiseEvent(Envelope.Create(@event)); - } - - protected virtual void RaiseEvent(Envelope @event) - { - Guard.NotNull(@event, nameof(@event)); - - @event.SetAggregateId(uniqueId); - - if (ApplyEvent(@event, false)) - { - uncomittedEvents.Add(@event); - } - } - - public IReadOnlyList> GetUncomittedEvents() - { - return uncomittedEvents; - } - - public void ClearUncommittedEvents() - { - uncomittedEvents.Clear(); - } - - protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, false); - } - - protected Task CreateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync()!, false); - } - - protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler.ToDefault(), false); - } - - protected Task Create(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, false); - } - - protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, true); - } - - protected Task UpdateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync()!, true); - } - - protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()!, true); - } - - protected Task Update(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, true); - } - - private async Task InvokeAsync(TCommand command, Func> handler, bool isUpdate) where TCommand : class, IAggregateCommand - { - Guard.NotNull(command, nameof(command)); - Guard.NotNull(handler, nameof(handler)); - - if (isUpdate) - { - await EnsureLoadedAsync(); - - if (Version < 0) - { - throw new DomainObjectNotFoundException(uniqueId.ToString()); - } - - if (Version != command.ExpectedVersion && command.ExpectedVersion > EtagVersion.Any) - { - throw new DomainObjectVersionException(uniqueId.ToString(), Version, command.ExpectedVersion); - } - - if (IsDeleted()) - { - throw new DomainException("Object has already been deleted."); - } - - if (!CanAccept(command)) - { - throw new DomainException("Invalid command."); - } - } - else - { - command.ExpectedVersion = EtagVersion.Empty; - - if (Version != command.ExpectedVersion) - { - throw new DomainObjectConflictException(uniqueId.ToString()); - } - - if (!CanAcceptCreation(command)) - { - throw new DomainException("Invalid command."); - } - } - - var previousSnapshot = Snapshot; - var previousVersion = Version; - try - { - var result = await handler(command); - - var events = uncomittedEvents.ToArray(); - - await WriteAsync(events, previousVersion); - - if (result == null) - { - if (isUpdate) - { - result = new EntitySavedResult(Version); - } - else - { - result = EntityCreatedResult.Create(uniqueId, Version); - } - } - - isLoaded = true; - - return result; - } - catch (InconsistentStateException) when (!isUpdate) - { - RestorePreviousSnapshot(previousSnapshot, previousVersion); - - throw new DomainObjectConflictException(uniqueId.ToString()); - } - catch - { - RestorePreviousSnapshot(previousSnapshot, previousVersion); - - throw; - } - finally - { - ClearUncommittedEvents(); - } - } - - protected virtual bool CanAcceptCreation(ICommand command) - { - return true; - } - - protected virtual bool CanAccept(ICommand command) - { - return true; - } - - protected virtual bool IsDeleted() - { - return false; - } - - protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); - - protected abstract bool ApplyEvent(Envelope @event, bool isLoading); - - protected abstract Task ReadAsync(); - - protected abstract Task WriteAsync(Envelope[] newEvents, long previousVersion); - - public virtual Task RebuildStateAsync() - { - return Task.CompletedTask; - } - - protected virtual void OnSetup() - { - } - - public abstract Task ExecuteAsync(IAggregateCommand command); - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index bec18cb74..e2bf063ea 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -12,18 +12,18 @@ using Squidex.Infrastructure.Orleans; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectGrain : GrainOfString where T : DomainObjectBase where TState : class, IDomainState, new() + public abstract class DomainObjectGrain : GrainOfString where T : DomainObject where TState : class, IDomainState, new() { private readonly T domainObject; public TState Snapshot { - get { return domainObject.Snapshot; } + get => domainObject.Snapshot; } protected T DomainObject { - get { return domainObject; } + get => domainObject; } protected DomainObjectGrain(IServiceProvider serviceProvider) @@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Commands return base.OnActivateAsync(key); } - public async Task> ExecuteAsync(J request) + public async Task> ExecuteAsync(J request) { request.Value.ApplyContext(); diff --git a/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs b/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs deleted file mode 100644 index 43b20e54e..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Commands -{ - public static class EntityCreatedResult - { - public static EntityCreatedResult Create(T idOrValue, long version) - { - return new EntityCreatedResult(idOrValue, version); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs b/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs deleted file mode 100644 index 2ab583d59..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Commands -{ - public class EntityCreatedResult : EntitySavedResult - { - public T IdOrValue { get; } - - public EntityCreatedResult(T idOrValue, long version) - : base(version) - { - IdOrValue = idOrValue; - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs index 75e84c987..7b73acc6a 100644 --- a/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs @@ -34,11 +34,18 @@ namespace Squidex.Infrastructure.Commands { var result = await ExecuteCommandAsync(typedCommand); - context.Complete(result); + var payload = await EnrichResultAsync(context, result); + + context.Complete(payload); } } - private async Task ExecuteCommandAsync(TCommand typedCommand) + protected virtual Task EnrichResultAsync(CommandContext context, CommandResult result) + { + return Task.FromResult(result.Payload is None ? result : result.Payload); + } + + private async Task ExecuteCommandAsync(TCommand typedCommand) { var grain = grainFactory.GetGrain(typedCommand.AggregateId.ToString()); diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs index 26836b106..428c57107 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs @@ -13,6 +13,6 @@ namespace Squidex.Infrastructure.Commands { public interface IDomainObjectGrain : IGrainWithStringKey { - Task> ExecuteAsync(J request); + Task> ExecuteAsync(J request); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/Is.cs b/backend/src/Squidex.Infrastructure/Commands/Is.cs index 0b73fe453..309672b1e 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Is.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Is.cs @@ -12,19 +12,19 @@ namespace Squidex.Infrastructure.Commands { public static class Is { - public static bool Change(DomainId oldValue, DomainId newValue) + public static bool Change(T oldValue, T newValue) { return !Equals(oldValue, newValue); } - public static bool Change(string? oldValue, string? newValue) + public static bool OptionalChange(T oldValue, [NotNullWhen(true)] T? newValue) where T : struct { - return !Equals(oldValue, newValue); + return newValue != null && !Equals(oldValue, newValue.Value); } - public static bool OptionalChange(bool oldValue, [NotNullWhen(true)] bool? newValue) + public static bool OptionalChange(T oldValue, [NotNullWhen(true)] T? newValue) where T : class { - return newValue.HasValue && oldValue != newValue.Value; + return newValue != null && !Equals(oldValue, newValue); } public static bool OptionalChange(string oldValue, [NotNullWhen(true)] string? newValue) @@ -32,12 +32,12 @@ namespace Squidex.Infrastructure.Commands return !string.IsNullOrWhiteSpace(newValue) && !string.Equals(oldValue, newValue); } - public static bool OptionalChange(ISet oldValue, [NotNullWhen(true)] ISet? newValue) + public static bool OptionalSetChange(ISet oldValue, [NotNullWhen(true)] ISet? newValue) { return newValue != null && !newValue.SetEquals(oldValue); } - public static bool OptionalChange(IReadOnlyDictionary oldValue, [NotNullWhen(true)] IReadOnlyDictionary? newValue) where TKey : notnull + public static bool OptionalMapChange(IReadOnlyDictionary oldValue, [NotNullWhen(true)] IReadOnlyDictionary? newValue) where TKey : notnull { return newValue != null && !newValue.EqualsDictionary(oldValue); } diff --git a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs deleted file mode 100644 index 2ac891ad0..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Log; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class LogSnapshotDomainObject : DomainObjectBase where T : class, IDomainState, new() - { - private readonly IStore store; - private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; - private IPersistence? persistence; - - public override T Snapshot - { - get { return snapshots.Last(); } - } - - protected LogSnapshotDomainObject(IStore store, ISemanticLog log) - : base(log) - { - Guard.NotNull(log, nameof(log)); - - this.store = store; - } - - protected override void OnSetup() - { - persistence = store.WithEventSourcing(GetType(), UniqueId, x => ApplyEvent(x, true)); - } - - public T GetSnapshot(long version) - { - if (version == EtagVersion.Any || version == EtagVersion.Auto) - { - return Snapshot; - } - - if (version == EtagVersion.Empty) - { - return snapshots[0]; - } - - if (version >= 0 && version < snapshots.Count - 1) - { - return snapshots[(int)version + 1]; - } - - return default!; - } - - protected sealed override bool ApplyEvent(Envelope @event, bool isLoading) - { - var snapshot = OnEvent(@event); - - if (!ReferenceEquals(Snapshot, snapshot) || isLoading) - { - var newVersion = Version + 1; - - snapshot.Version = newVersion; - snapshots.Add(snapshot); - - return true; - } - - return false; - } - - protected sealed override async Task WriteAsync(Envelope[] newEvents, long previousVersion) - { - if (newEvents.Length > 0 && persistence != null) - { - var persistedSnapshots = store.GetSnapshotStore(); - - await persistence.WriteEventsAsync(newEvents); - await persistedSnapshots.WriteAsync(UniqueId, Snapshot, previousVersion, Snapshot.Version); - } - } - - protected sealed override async Task ReadAsync() - { - if (persistence != null) - { - await persistence.ReadAsync(); - } - } - - public sealed override async Task RebuildStateAsync() - { - await EnsureLoadedAsync(true); - - if (Snapshot.Version <= EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(UniqueId.ToString()); - } - - if (persistence != null) - { - var persistedSnapshots = store.GetSnapshotStore(); - - await persistedSnapshots.WriteAsync(UniqueId, Snapshot, EtagVersion.Any, Snapshot.Version); - } - } - - protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) - { - while (snapshots.Count > previousVersion + 2) - { - snapshots.RemoveAt(snapshots.Count - 1); - } - } - - protected T OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs index a0b25b200..a68491e1d 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs @@ -41,7 +41,7 @@ namespace Squidex.Infrastructure.Commands this.store = store; } - public virtual async Task RebuildAsync(string filter, CancellationToken ct) where T : DomainObjectBase where TState : class, IDomainState, new() + public virtual async Task RebuildAsync(string filter, CancellationToken ct) where T : DomainObject where TState : class, IDomainState, new() { await store.GetSnapshotStore().ClearAsync(); @@ -56,7 +56,7 @@ namespace Squidex.Infrastructure.Commands }, ct); } - public virtual async Task InsertManyAsync(IEnumerable source, CancellationToken ct = default) where T : DomainObjectBase where TState : class, IDomainState, new() + public virtual async Task InsertManyAsync(IEnumerable source, CancellationToken ct = default) where T : DomainObject where TState : class, IDomainState, new() { Guard.NotNull(source, nameof(source)); @@ -69,7 +69,7 @@ namespace Squidex.Infrastructure.Commands }, ct); } - private async Task InsertManyAsync(IdSource source, CancellationToken ct = default) where T : DomainObjectBase where TState : class, IDomainState, new() + private async Task InsertManyAsync(IdSource source, CancellationToken ct = default) where T : DomainObject where TState : class, IDomainState, new() { var worker = new ActionBlock(async id => { diff --git a/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs b/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs new file mode 100644 index 000000000..cfe42373f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class SnapshotList where T : class, IDomainState, new() + { + private readonly List items = new List(2); + private int capacity = 2; + + public int Capacity + { + get => capacity; + set + { + value = Math.Max(1, value); + + if (capacity != value) + { + capacity = value; + + Clean(); + } + } + } + + public long Version + { + get => items.Count - 2; + } + + public T Current + { + get => items.Last()!; + } + + public SnapshotList() + { + Clear(); + } + + public void Clear() + { + items.Clear(); + items.Add(new T { Version = EtagVersion.Empty }); + } + + public (T?, bool Valid) Get(long version) + { + if (version == EtagVersion.Any || version == EtagVersion.Auto) + { + return (Current, true); + } + + var index = GetIndex(version); + + if (index >= 0 && index < items.Count) + { + return (items[index], true); + } + + return (null, false); + } + + public bool Contains(long version) + { + var index = GetIndex(version); + + return items.ElementAtOrDefault(index) != null; + } + + public void Add(T snapshot, long version, bool clean = false) + { + var index = GetIndex(version); + + while (items.Count <= index) + { + items.Add(null); + } + + items[index] = snapshot; + + if (clean) + { + Clean(); + } + } + + public void ResetTo(T snapshot, long version) + { + var index = GetIndex(version); + + while (items.Count > index + 1) + { + items.RemoveAt(items.Count - 1); + } + + items[index] = snapshot; + } + + private void Clean() + { + var lastIndex = items.Count - 1; + + for (var i = lastIndex - capacity; i >= 0; i--) + { + items[i] = null; + } + } + + private static int GetIndex(long version) + { + return (int)(version + 1); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/DomainObjectException.cs b/backend/src/Squidex.Infrastructure/DomainObjectException.cs index 18d6069c7..f5ad3527c 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectException.cs @@ -11,7 +11,7 @@ using System.Runtime.Serialization; namespace Squidex.Infrastructure { [Serializable] - public class DomainObjectException : Exception + public class DomainObjectException : DomainException { public string Id { get; } diff --git a/backend/src/Squidex.Infrastructure/EtagVersion.cs b/backend/src/Squidex.Infrastructure/EtagVersion.cs index 21c305132..8c0463ce0 100644 --- a/backend/src/Squidex.Infrastructure/EtagVersion.cs +++ b/backend/src/Squidex.Infrastructure/EtagVersion.cs @@ -9,8 +9,6 @@ namespace Squidex.Infrastructure { public static class EtagVersion { - public const long NotFound = long.MinValue; - public const long Auto = -3; public const long Any = -2; diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs index 4310f2c5b..c81aa79e1 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs @@ -14,12 +14,12 @@ namespace Squidex.Infrastructure.EventSourcing public EnvelopeHeaders Headers { - get { return headers; } + get => headers; } public T Payload { - get { return payload; } + get => payload; } public Envelope(T payload, EnvelopeHeaders? headers = null) diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs index 3657c27bc..174d5d237 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs @@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains public object? Sender { - get { return eventSubscription.Sender!; } + get => eventSubscription.Sender!; } private sealed class Job diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs index 67770a3dc..e11d89501 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs @@ -22,12 +22,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains public bool IsPaused { - get { return IsStopped && string.IsNullOrWhiteSpace(Error); } + get => IsStopped && string.IsNullOrWhiteSpace(Error); } public bool IsFailed { - get { return IsStopped && !string.IsNullOrWhiteSpace(Error); } + get => IsStopped && !string.IsNullOrWhiteSpace(Error); } public EventConsumerState() diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs index 478e07328..d65f2805e 100644 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs @@ -29,7 +29,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft public virtual IEnumerable SupportedTypes { - get { return supportedTypes; } + get => supportedTypes; } public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs index 74fb1f4d9..32bbfffe6 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs @@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.Json.Objects { public JsonValueType Type { - get { return JsonValueType.Array; } + get => JsonValueType.Array; } public JsonArray() diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs index de4f1ddf0..cbd1c4000 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Json.Objects public override JsonValueType Type { - get { return JsonValueType.Boolean; } + get => JsonValueType.Boolean; } private JsonBoolean(bool value) diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs index 04b4f18c2..9774b1536 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs @@ -16,7 +16,7 @@ namespace Squidex.Infrastructure.Json.Objects public JsonValueType Type { - get { return JsonValueType.Null; } + get => JsonValueType.Null; } private JsonNull() diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs index 25c3c573f..440398858 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs @@ -13,7 +13,7 @@ namespace Squidex.Infrastructure.Json.Objects { public override JsonValueType Type { - get { return JsonValueType.Number; } + get => JsonValueType.Number; } internal JsonNumber(double value) diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs index 3dda169e5..e476293e7 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs @@ -33,22 +33,22 @@ namespace Squidex.Infrastructure.Json.Objects public IEnumerable Keys { - get { return inner.Keys; } + get => inner.Keys; } public IEnumerable Values { - get { return inner.Values; } + get => inner.Values; } public int Count { - get { return inner.Count; } + get => inner.Count; } public JsonValueType Type { - get { return JsonValueType.Object; } + get => JsonValueType.Object; } public JsonObject() diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs index 4aeccc93f..ccb87180a 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs @@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.Json.Objects { public override JsonValueType Type { - get { return JsonValueType.String; } + get => JsonValueType.String; } internal JsonString(string value) diff --git a/backend/src/Squidex.Infrastructure/Language.cs b/backend/src/Squidex.Infrastructure/Language.cs index 7eb6855bc..6b8d7ab77 100644 --- a/backend/src/Squidex.Infrastructure/Language.cs +++ b/backend/src/Squidex.Infrastructure/Language.cs @@ -34,14 +34,14 @@ namespace Squidex.Infrastructure public static IReadOnlyCollection AllLanguages { - get { return AllLanguagesField.Values; } + get => AllLanguagesField.Values; } public string Iso2Code { get; } public string EnglishName { - get { return AllLanguagesNames.GetOrDefault(Iso2Code) ?? string.Empty; } + get => AllLanguagesNames.GetOrDefault(Iso2Code) ?? string.Empty; } private Language(string iso2Code) diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs index a62ef9326..54f2d9a95 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs @@ -25,7 +25,7 @@ namespace Squidex.Infrastructure.Orleans public long Version { - get { return persistence.Version; } + get => persistence.Version; } public GrainState(IGrainActivationContext context) diff --git a/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs index e2d2aec5b..f4f981062 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs @@ -17,17 +17,17 @@ namespace Squidex.Infrastructure.Orleans public override bool CanRead { - get { return true; } + get => true; } public override bool CanSeek { - get { return false; } + get => false; } public override bool CanWrite { - get { return false; } + get => false; } public override long Length diff --git a/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs index fe9707a47..6fdd2f18b 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs @@ -17,22 +17,22 @@ namespace Squidex.Infrastructure.Orleans public override bool CanRead { - get { return false; } + get => false; } public override bool CanSeek { - get { return false; } + get => false; } public override bool CanWrite { - get { return true; } + get => true; } public override long Length { - get { return writer.CurrentOffset; } + get => writer.CurrentOffset; } public override long Position diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs index 044a33080..69ecb548a 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.Queries.OData { var top = query.ParseTop(); - if (top.HasValue) + if (top != null) { result.Take = top.Value; } @@ -25,7 +25,7 @@ namespace Squidex.Infrastructure.Queries.OData { var skip = query.ParseSkip(); - if (skip.HasValue) + if (skip != null) { result.Skip = skip.Value; } diff --git a/backend/src/Squidex.Infrastructure/Queries/Query.cs b/backend/src/Squidex.Infrastructure/Queries/Query.cs index 136999e10..990473253 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Query.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Query.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Queries public long Top { - set { Take = value; } + set => Take = value; } public List Sort { get; set; } = new List(); diff --git a/backend/src/Squidex.Infrastructure/RefToken.cs b/backend/src/Squidex.Infrastructure/RefToken.cs index 9ecf64f10..11cfe1db1 100644 --- a/backend/src/Squidex.Infrastructure/RefToken.cs +++ b/backend/src/Squidex.Infrastructure/RefToken.cs @@ -22,12 +22,12 @@ namespace Squidex.Infrastructure public bool IsClient { - get { return Type == RefTokenType.Client; } + get => Type == RefTokenType.Client; } public bool IsUser { - get { return Type == RefTokenType.Subject; } + get => Type == RefTokenType.Subject; } public RefToken(RefTokenType type, string identifier) diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.cs b/backend/src/Squidex.Infrastructure/Security/Permission.cs index 1261b7273..de7744086 100644 --- a/backend/src/Squidex.Infrastructure/Security/Permission.cs +++ b/backend/src/Squidex.Infrastructure/Security/Permission.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.Security private Part[] Path { - get { return path ??= Part.ParsePath(Id); } + get => path ??= Part.ParsePath(Id); } public Permission(string id) diff --git a/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs index 1452a961b..c90896185 100644 --- a/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs +++ b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Security public int Count { - get { return permissions.Count; } + get => permissions.Count; } public PermissionSet(params Permission[] permissions) diff --git a/backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs b/backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs index a804dece1..c21e7c2f7 100644 --- a/backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs +++ b/backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs @@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.States Task DeleteAsync(); - Task WriteEventsAsync(IEnumerable> events); + Task WriteEventsAsync(IReadOnlyList> events); Task WriteSnapshotAsync(TState state); diff --git a/backend/src/Squidex.Infrastructure/States/IStore.cs b/backend/src/Squidex.Infrastructure/States/IStore.cs index 892770065..f659c392e 100644 --- a/backend/src/Squidex.Infrastructure/States/IStore.cs +++ b/backend/src/Squidex.Infrastructure/States/IStore.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public delegate void HandleEvent(Envelope @event); + public delegate bool HandleEvent(Envelope @event); public delegate void HandleSnapshot(T state); diff --git a/backend/src/Squidex.Infrastructure/States/Persistence.cs b/backend/src/Squidex.Infrastructure/States/Persistence.cs index e388cf7be..3b974de49 100644 --- a/backend/src/Squidex.Infrastructure/States/Persistence.cs +++ b/backend/src/Squidex.Infrastructure/States/Persistence.cs @@ -14,12 +14,12 @@ namespace Squidex.Infrastructure.States { public Persistence(TKey ownerKey, Type ownerType, IEventStore eventStore, - IEventEnricher eventEnricher, IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, HandleEvent? applyEvent) - : base(ownerKey, ownerType, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) + : base(ownerKey, ownerType, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, + PersistenceMode.EventSourcing, null, applyEvent) { } } diff --git a/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs b/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs index 22d10548b..48d74d29d 100644 --- a/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs +++ b/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs @@ -5,12 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + namespace Squidex.Infrastructure.States { + [Flags] public enum PersistenceMode { - EventSourcing, - Snapshots, - SnapshotsAndEventSourcing + EventSourcing = 1, + Snapshots = 2, + SnapshotsAndEventSourcing = 3 } } diff --git a/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs index ec7495678..7ef7e2452 100644 --- a/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs +++ b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs @@ -18,27 +18,34 @@ namespace Squidex.Infrastructure.States internal class Persistence : IPersistence where TKey : notnull { private readonly TKey ownerKey; - private readonly Type ownerType; private readonly ISnapshotStore snapshotStore; - private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; - private readonly IEventEnricher eventEnricher; private readonly IEventDataFormatter eventDataFormatter; private readonly PersistenceMode persistenceMode; private readonly HandleSnapshot? applyState; private readonly HandleEvent? applyEvent; + private readonly Lazy streamName; private long versionSnapshot = EtagVersion.Empty; private long versionEvents = EtagVersion.Empty; private long version = EtagVersion.Empty; public long Version { - get { return version; } + get => version; + } + + private bool UseSnapshots + { + get => (persistenceMode & PersistenceMode.Snapshots) == PersistenceMode.Snapshots; + } + + private bool UseEventSourcing + { + get => (persistenceMode & PersistenceMode.EventSourcing) == PersistenceMode.EventSourcing; } public Persistence(TKey ownerKey, Type ownerType, IEventStore eventStore, - IEventEnricher eventEnricher, IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, @@ -47,15 +54,27 @@ namespace Squidex.Infrastructure.States HandleEvent? applyEvent) { this.ownerKey = ownerKey; - this.ownerType = ownerType; this.applyState = applyState; this.applyEvent = applyEvent; this.eventStore = eventStore; - this.eventEnricher = eventEnricher; this.eventDataFormatter = eventDataFormatter; this.persistenceMode = persistenceMode; this.snapshotStore = snapshotStore; - this.streamNameResolver = streamNameResolver; + + streamName = new Lazy(() => streamNameResolver.GetStreamName(ownerType, ownerKey.ToString()!)); + } + + public async Task DeleteAsync() + { + if (UseSnapshots) + { + await snapshotStore.RemoveAsync(ownerKey); + } + + if (UseEventSourcing) + { + await eventStore.DeleteStreamAsync(streamName.Value); + } } public async Task ReadAsync(long expectedVersion = EtagVersion.Any) @@ -63,8 +82,15 @@ namespace Squidex.Infrastructure.States versionSnapshot = EtagVersion.Empty; versionEvents = EtagVersion.Empty; - await ReadSnapshotAsync(); - await ReadEventsAsync(); + if (UseSnapshots) + { + await ReadSnapshotAsync(); + } + + if (UseEventSourcing) + { + await ReadEventsAsync(); + } UpdateVersion(); @@ -83,130 +109,97 @@ namespace Squidex.Infrastructure.States private async Task ReadSnapshotAsync() { - if (UseSnapshots()) - { - var (state, position) = await snapshotStore.ReadAsync(ownerKey); + var (state, position) = await snapshotStore.ReadAsync(ownerKey); - if (position < EtagVersion.Empty) - { - position = EtagVersion.Empty; - } + // Treat all negative values as not-found (empty). + position = Math.Max(position, EtagVersion.Empty); - versionSnapshot = position; - versionEvents = position; + versionSnapshot = position; + versionEvents = position; - if (applyState != null && position >= 0) - { - applyState(state); - } + if (applyState != null && position >= 0) + { + applyState(state); } } private async Task ReadEventsAsync() { - if (UseEventSourcing()) + var events = await eventStore.QueryAsync(streamName.Value, versionEvents + 1); + + var isStopped = false; + + foreach (var @event in events) { - var events = await eventStore.QueryAsync(GetStreamName(), versionEvents + 1); + var newVersion = versionEvents + 1; - foreach (var @event in events) + if (@event.EventStreamNumber != newVersion) { - versionEvents++; - - if (@event.EventStreamNumber != versionEvents) - { - throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); - } + throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); + } + // Skip the parsing for performance reasons if we are not interested, but continue reading to get the version. + if (!isStopped) + { var parsedEvent = eventDataFormatter.ParseIfKnown(@event); if (applyEvent != null && parsedEvent != null) { - applyEvent(parsedEvent); + isStopped = !applyEvent(parsedEvent); } } + + versionEvents++; } } public async Task WriteSnapshotAsync(TSnapshot state) { - var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; + var oldVersion = versionSnapshot; - if (newVersion != versionSnapshot) + if (oldVersion == EtagVersion.Empty && UseEventSourcing) { - await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); + oldVersion = (versionEvents - 1); + } + + var newVersion = UseEventSourcing ? versionEvents : oldVersion + 1; - versionSnapshot = newVersion; + if (newVersion == versionSnapshot) + { + return; } + await snapshotStore.WriteAsync(ownerKey, state, oldVersion, newVersion); + + versionSnapshot = newVersion; + UpdateVersion(); } - public async Task WriteEventsAsync(IEnumerable> events) + public async Task WriteEventsAsync(IReadOnlyList> events) { - Guard.NotNull(events, nameof(events)); + Guard.NotEmpty(events, nameof(events)); - var eventArray = events.ToArray(); + var oldVersion = EtagVersion.Any; - if (eventArray.Length > 0) + if (UseEventSourcing) { - var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; - - var commitId = Guid.NewGuid(); - - foreach (var @event in eventArray) - { - eventEnricher.Enrich(@event, ownerKey); - } - - var eventStream = GetStreamName(); - var eventData = GetEventData(eventArray, commitId); - - try - { - await eventStore.AppendAsync(commitId, eventStream, expectedVersion, eventData); - } - catch (WrongEventVersionException ex) - { - throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); - } - - versionEvents += eventArray.Length; + oldVersion = versionEvents; } - UpdateVersion(); - } + var eventCommitId = Guid.NewGuid(); + var eventData = events.Select(x => eventDataFormatter.ToEventData(x, eventCommitId, true)).ToArray(); - public async Task DeleteAsync() - { - if (UseEventSourcing()) + try { - await eventStore.DeleteStreamAsync(GetStreamName()); + await eventStore.AppendAsync(eventCommitId, streamName.Value, oldVersion, eventData); } - - if (UseSnapshots()) + catch (WrongEventVersionException ex) { - await snapshotStore.RemoveAsync(ownerKey); + throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); } - } - private EventData[] GetEventData(Envelope[] events, Guid commitId) - { - return events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray(); - } - - private string GetStreamName() - { - return streamNameResolver.GetStreamName(ownerType, ownerKey.ToString()!); - } - - private bool UseSnapshots() - { - return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private bool UseEventSourcing() - { - return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + versionEvents += eventData.Length; } private void UpdateVersion() diff --git a/backend/src/Squidex.Infrastructure/States/Store.cs b/backend/src/Squidex.Infrastructure/States/Store.cs index b9b88e11d..2663bbe77 100644 --- a/backend/src/Squidex.Infrastructure/States/Store.cs +++ b/backend/src/Squidex.Infrastructure/States/Store.cs @@ -15,18 +15,20 @@ namespace Squidex.Infrastructure.States private readonly IServiceProvider services; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; - private readonly IEventEnricher eventEnricher; private readonly IEventDataFormatter eventDataFormatter; public Store( IEventStore eventStore, - IEventEnricher eventEnricher, IEventDataFormatter eventDataFormatter, IServiceProvider services, IStreamNameResolver streamNameResolver) { + Guard.NotNull(eventStore, nameof(eventStore)); + Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); + Guard.NotNull(services, nameof(services)); + Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); + this.eventStore = eventStore; - this.eventEnricher = eventEnricher; this.eventDataFormatter = eventDataFormatter; this.services = services; this.streamNameResolver = streamNameResolver; @@ -44,7 +46,8 @@ namespace Squidex.Infrastructure.States public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot? applySnapshot, HandleEvent? applyEvent) { - return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, + applySnapshot, applyEvent); } private IPersistence CreatePersistence(Type owner, TKey key, HandleEvent? applyEvent) @@ -53,7 +56,8 @@ namespace Squidex.Infrastructure.States var snapshotStore = GetSnapshotStore(); - return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); + return new Persistence(key, owner, eventStore, eventDataFormatter, + snapshotStore, streamNameResolver, applyEvent); } private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, HandleSnapshot? applySnapshot, HandleEvent? applyEvent) @@ -62,7 +66,8 @@ namespace Squidex.Infrastructure.States var snapshotStore = GetSnapshotStore(); - return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); + return new Persistence(key, owner, eventStore, eventDataFormatter, + snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); } public ISnapshotStore GetSnapshotStore() diff --git a/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs index c7ed09805..da818d2b6 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs @@ -20,12 +20,7 @@ namespace Squidex.Infrastructure.Tasks public Task Completion { - get { return Task.WhenAll(workers.Select(x => x.Completion)); } - } - - public PartitionedActionBlock(Action action, Func partitioner) - : this (action?.ToAsync()!, partitioner, new ExecutionDataflowBlockOptions()) - { + get => Task.WhenAll(workers.Select(x => x.Completion)); } public PartitionedActionBlock(Func action, Func partitioner) @@ -33,11 +28,6 @@ namespace Squidex.Infrastructure.Tasks { } - public PartitionedActionBlock(Action action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) - : this(action?.ToAsync()!, partitioner, dataflowBlockOptions) - { - } - public PartitionedActionBlock(Func action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) { Guard.NotNull(action, nameof(action)); diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index ef56f9c5c..a062bfb5f 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -34,52 +34,6 @@ namespace Squidex.Infrastructure.Tasks } } - public static Func ToDefault(this Action action) - { - Guard.NotNull(action, nameof(action)); - - return x => - { - action(x); - - return default!; - }; - } - - public static Func> ToDefault(this Func action) - { - Guard.NotNull(action, nameof(action)); - - return async x => - { - await action(x); - - return default!; - }; - } - - public static Func> ToAsync(this Func action) - { - Guard.NotNull(action, nameof(action)); - - return x => - { - var result = action(x); - - return Task.FromResult(result); - }; - } - - public static Func ToAsync(this Action action) - { - return x => - { - action(x); - - return Task.CompletedTask; - }; - } - public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs index 21db20f6c..cf39f7649 100644 --- a/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs @@ -19,12 +19,12 @@ namespace Squidex.Infrastructure.Validation public string Message { - get { return message; } + get => message; } public IEnumerable PropertyNames { - get { return propertyNames; } + get => propertyNames; } public ValidationError(string message, params string[] propertyNames) diff --git a/backend/src/Squidex.Shared/Permissions.cs b/backend/src/Squidex.Shared/Permissions.cs index 7873b9ad2..1b3097bac 100644 --- a/backend/src/Squidex.Shared/Permissions.cs +++ b/backend/src/Squidex.Shared/Permissions.cs @@ -21,12 +21,12 @@ namespace Squidex.Shared public static IReadOnlyList ForAppsNonSchema { - get { return ForAppsNonSchemaList; } + get => ForAppsNonSchemaList; } public static IReadOnlyList ForAppsSchema { - get { return ForAppsSchemaList; } + get => ForAppsSchemaList; } public const string All = "squidex.*"; diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 52a16fd6b..72de2ad4b 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -496,8 +496,8 @@ Il contenuto singleton non può essere eliminato. - - L'ora deve essere futura. + + Status is not defined in the workflow. Non è possibile cambiare stato da {oldStatus} a {newStatus}. @@ -631,7 +631,7 @@ Deve essere tra {min} e {max} parola(e). - + Il workflow del contenuto impedisce la pubblicazione. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index c09135f08..f65134dbd 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -496,8 +496,8 @@ Singleton-inhoud kan niet worden verwijderd. - - De tijd moet in de toekomst liggen. + + Status is not defined in the workflow. Kan status niet wijzigen van {oldStatus} in {newStatus}. @@ -631,7 +631,7 @@ Moet tussen {min} en {max} woord (en) bevatten. - + Contentworkflow verhindert publiceren. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index ec883bd09..0cc451444 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -496,8 +496,8 @@ Singleton content cannot be deleted. - - Due time must be in the future. + + Status is not defined in the workflow. Cannot change status from {oldStatus} to {newStatus}. @@ -631,7 +631,7 @@ Must have between {min} and {max} word(s). - + Content workflow prevents publishing. diff --git a/backend/src/Squidex.Shared/Users/ClientUser.cs b/backend/src/Squidex.Shared/Users/ClientUser.cs index 52419312e..d2ab35cce 100644 --- a/backend/src/Squidex.Shared/Users/ClientUser.cs +++ b/backend/src/Squidex.Shared/Users/ClientUser.cs @@ -20,22 +20,22 @@ namespace Squidex.Shared.Users public string Id { - get { return token.Identifier; } + get => token.Identifier; } public string Email { - get { return token.ToString(); } + get => token.ToString(); } public bool IsLocked { - get { return false; } + get => false; } public IReadOnlyList Claims { - get { return claims; } + get => claims; } public object Identity => throw new System.NotImplementedException(); diff --git a/backend/src/Squidex.Web/ApiController.cs b/backend/src/Squidex.Web/ApiController.cs index 29e37a8fe..942e329b7 100644 --- a/backend/src/Squidex.Web/ApiController.cs +++ b/backend/src/Squidex.Web/ApiController.cs @@ -42,17 +42,17 @@ namespace Squidex.Web protected Resources Resources { - get { return resources.Value; } + get => resources.Value; } protected Context Context { - get { return HttpContext.Context(); } + get => HttpContext.Context(); } protected DomainId AppId { - get { return App.Id; } + get => App.Id; } protected ApiController(ICommandBus commandBus) diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index 6198b6132..aabbaa28d 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -21,7 +21,7 @@ namespace Squidex.Web public IEnumerable PermissionIds { - get { return permissionIds; } + get => permissionIds; } public ApiPermissionAttribute(params string[] ids) diff --git a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs index 17bef171c..08c7108b9 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs @@ -54,9 +54,9 @@ namespace Squidex.Web.CommandMiddlewares await next(context); - if (context.PlainResult is EntitySavedResult result) + if (context.PlainResult is CommandResult result) { - SetResponsEtag(httpContext, result.Version); + SetResponsEtag(httpContext, result.NewVersion); } else if (context.PlainResult is IEntityWithVersion entity) { diff --git a/backend/src/Squidex.Web/Deferred.cs b/backend/src/Squidex.Web/Deferred.cs index bda383f0e..b371af54f 100644 --- a/backend/src/Squidex.Web/Deferred.cs +++ b/backend/src/Squidex.Web/Deferred.cs @@ -17,7 +17,7 @@ namespace Squidex.Web public Task Value { - get { return value.Value; } + get => value.Value; } private Deferred(Func> value) diff --git a/backend/src/Squidex.Web/EntityCreatedDto.cs b/backend/src/Squidex.Web/EntityCreatedDto.cs deleted file mode 100644 index 615e8f190..000000000 --- a/backend/src/Squidex.Web/EntityCreatedDto.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Web -{ - public sealed class EntityCreatedDto - { - [LocalizedRequired] - [Display(Description = "Id of the created entity.")] - public string? Id { get; set; } - - [Display(Description = "The new version of the entity.")] - public long Version { get; set; } - - public static EntityCreatedDto FromResult(EntityCreatedResult result) - { - return new EntityCreatedDto { Id = result.IdOrValue?.ToString(), Version = result.Version }; - } - } -} diff --git a/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs b/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs index dd58e2d2e..fc08726bd 100644 --- a/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs +++ b/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs @@ -26,7 +26,7 @@ namespace Squidex.Web.Pipeline { try { - var (range, _, serveBody) = SetHeadersAndLog(context, result, result.FileSize, result.FileSize.HasValue); + var (range, _, serveBody) = SetHeadersAndLog(context, result, result.FileSize, result.FileSize != null); if (!string.IsNullOrWhiteSpace(result.FileDownloadName) && result.SendInline) { diff --git a/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs b/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs index b31ec4fb9..2e09d5bab 100644 --- a/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs +++ b/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs @@ -19,7 +19,7 @@ namespace Squidex.Web.Pipeline public long BytesWritten { - get { return bytesWritten; } + get => bytesWritten; } public UsagePipeWriter(PipeWriter inner) diff --git a/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs b/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs index 7db7ddf36..f610831f3 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs @@ -22,17 +22,17 @@ namespace Squidex.Web.Pipeline public long BytesWritten { - get { return bytesWritten + usageStream.BytesWritten + usageWriter.BytesWritten; } + get => bytesWritten + usageStream.BytesWritten + usageWriter.BytesWritten; } public Stream Stream { - get { return usageStream; } + get => usageStream; } public PipeWriter Writer { - get { return usageWriter; } + get => usageWriter; } public UsageResponseBodyFeature(IHttpResponseBodyFeature inner) diff --git a/backend/src/Squidex.Web/Pipeline/UsageStream.cs b/backend/src/Squidex.Web/Pipeline/UsageStream.cs index d63b5fe93..77661f81f 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageStream.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageStream.cs @@ -21,22 +21,22 @@ namespace Squidex.Web.Pipeline public long BytesWritten { - get { return bytesWritten; } + get => bytesWritten; } public override bool CanRead { - get { return false; } + get => false; } public override bool CanSeek { - get { return false; } + get => false; } public override bool CanWrite { - get { return inner.CanWrite; } + get => inner.CanWrite; } public override long Length diff --git a/backend/src/Squidex.Web/Services/StringLocalizer.cs b/backend/src/Squidex.Web/Services/StringLocalizer.cs index 51b55cd84..15a0b1362 100644 --- a/backend/src/Squidex.Web/Services/StringLocalizer.cs +++ b/backend/src/Squidex.Web/Services/StringLocalizer.cs @@ -23,7 +23,7 @@ namespace Squidex.Web.Services public LocalizedString this[string name] { - get { return this[name, null!]; } + get => this[name, null!]; } public LocalizedString this[string name, params object[] arguments] diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs index ce46335c1..2d9872dca 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs @@ -18,30 +18,37 @@ namespace Squidex.Areas.Api.Config.OpenApi { public void Process(DocumentProcessorContext context) { - foreach (var controllerType in context.ControllerTypes) + try { - var attribute = controllerType.GetCustomAttribute(); - - if (attribute != null) + foreach (var controllerType in context.ControllerTypes) { - var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); + var attribute = controllerType.GetCustomAttribute(); - if (tag != null) + if (attribute != null) { - var description = controllerType.GetXmlDocsSummary(); + var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); - if (description != null) + if (tag != null) { - tag.Description ??= string.Empty; + var description = controllerType.GetXmlDocsSummary(); - if (!tag.Description.Contains(description)) + if (description != null) { - tag.Description += "\n\n" + description; + tag.Description ??= string.Empty; + + if (!tag.Description.Contains(description)) + { + tag.Description += "\n\n" + description; + } } } } } } + finally + { + XmlDocs.ClearCache(); + } } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs index 595798419..d2946bd01 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -48,16 +48,14 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("apps/{app}/assets/folders", Order = -1)] - [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(AssetFoldersDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssetFolders(string app, [FromQuery] DomainId parentId) { - var (folders, path) = - await AsyncHelper.WhenAll( - assetQuery.QueryAssetFoldersAsync(Context, parentId), - assetQuery.FindAssetFolderAsync(Context.App.Id, parentId) - ); + var (folders, path) = await AsyncHelper.WhenAll( + assetQuery.QueryAssetFoldersAsync(Context, parentId), + assetQuery.FindAssetFolderAsync(Context.App.Id, parentId)); var response = Deferred.Response(() => { @@ -137,9 +135,9 @@ namespace Squidex.Areas.Api.Controllers.Assets [AssetRequestSizeLimit] [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] - public async Task PutAssetFolderParent(string app, DomainId id, [FromBody] MoveAssetItemDto request) + public async Task PutAssetFolderParent(string app, DomainId id, [FromBody] MoveAssetFolderDto request) { - var command = request.ToFolderCommand(id); + var command = request.ToCommand(id); var response = await InvokeCommandAsync(command); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index f19b63a2b..36e47f1a3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -206,6 +207,66 @@ namespace Squidex.Areas.Api.Controllers.Assets return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); } + /// + /// Bulk update assets. + /// + /// The name of the app. + /// The bulk update request. + /// + /// 200 => Assets created, update or delete. + /// 400 => Assets request not valid. + /// 404 => App not found. + /// + [HttpPost] + [Route("apps/{app}/assets/bulk")] + [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(Permissions.AppAssets)] + [ApiCosts(5)] + public async Task BulkUpdateAssets(string app, [FromBody] BulkUpdateAssetsDto request) + { + var command = request.ToCommand(); + + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = result.Select(x => BulkResultDto.FromBulkResult(x, HttpContext)).ToArray(); + + return Ok(response); + } + + /// + /// Upsert an asset. + /// + /// The name of the app. + /// The optional parent folder id. + /// The file to upload. + /// The optional custom asset id. + /// + /// 200 => Asset created or updated. + /// 400 => Asset request not valid. + /// 413 => Asset exceeds the maximum upload size. + /// 404 => App not found. + /// + /// + /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. + /// + [HttpPost] + [Route("apps/{app}/assets/{id}")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(Permissions.AppAssetsCreate)] + [ApiCosts(1)] + public async Task PostUpsertAsset(string app, DomainId id, [FromQuery] DomainId? parentId, IFormFile file) + { + var assetFile = await CheckAssetFileAsync(file); + + var command = new UpsertAsset { File = assetFile, ParentId = parentId, AssetId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + /// /// Replace asset content. /// @@ -280,7 +341,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [AssetRequestSizeLimit] [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] - public async Task PutAssetParent(string app, DomainId id, [FromBody] MoveAssetItemDto request) + public async Task PutAssetParent(string app, DomainId id, [FromBody] MoveAssetDto request) { var command = request.ToCommand(id); @@ -295,6 +356,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// The name of the app. /// The id of the asset to delete. /// True to check referrers of this asset. + /// True to delete the asset permanently. /// /// 204 => Asset deleted. /// 404 => Asset or app not found. @@ -303,9 +365,11 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/{id}/")] [ApiPermissionOrAnonymous(Permissions.AppAssetsDelete)] [ApiCosts(1)] - public async Task DeleteAsset(string app, DomainId id, [FromQuery] bool checkReferrers = false) + public async Task DeleteAsset(string app, DomainId id, + [FromQuery] bool checkReferrers = false, + [FromQuery] bool permanent = false) { - await CommandBus.PublishAsync(new DeleteAsset { AssetId = id, CheckReferrers = checkReferrers }); + await CommandBus.PublishAsync(new DeleteAsset { AssetId = id, CheckReferrers = checkReferrers, Permanent = false }); return NoContent(); } @@ -314,9 +378,9 @@ namespace Squidex.Areas.Api.Controllers.Assets { var context = await CommandBus.PublishAsync(command); - if (context.PlainResult is AssetCreatedResult created) + if (context.PlainResult is AssetDuplicate created) { - return AssetDto.FromAsset(created.Asset, Resources, created.IsDuplicate); + return AssetDto.FromAsset(created.Asset, Resources, true); } else { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs index 329f3c855..3056bdaa2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs @@ -111,7 +111,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models { if (!IgnoreFocus) { - if (FocusX.HasValue && FocusY.HasValue) + if (FocusX != null && FocusY != null) { return (FocusX.Value, FocusY.Value); } @@ -119,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models var focusX = asset.Metadata.GetFocusX(); var focusY = asset.Metadata.GetFocusY(); - if (focusX.HasValue && focusY.HasValue) + if (focusX != null && focusY != null) { return (focusX.Value, focusY.Value); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index 687dc1fa8..98dd3a0cf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -136,7 +136,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Obsolete("Use Type instead")] public bool IsImage { - get { return Type == AssetType.Image; } + get => Type == AssetType.Image; } /// @@ -145,7 +145,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Obsolete("Use Metdata instead")] public int? PixelWidth { - get { return Metadata.GetPixelWidth(); } + get => Metadata.GetPixelWidth(); } /// @@ -154,7 +154,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Obsolete("Use Metdata instead")] public int? PixelHeight { - get { return Metadata.GetPixelHeight(); } + get => Metadata.GetPixelHeight(); } public static AssetDto FromAsset(IEnrichedAssetEntity asset, Resources resources, bool isDuplicate = false) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs new file mode 100644 index 000000000..2d3678632 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class BulkUpdateAssetsDto + { + /// + /// The contents to update or insert. + /// + [LocalizedRequired] + public BulkUpdateAssetsJobDto[]? Jobs { get; set; } + + public BulkUpdateAssets ToCommand() + { + var result = SimpleMapper.Map(this, new BulkUpdateAssets()); + + result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray(); + + return result; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs new file mode 100644 index 000000000..f173a0a55 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public class BulkUpdateAssetsJobDto + { + /// + /// An optional id of the asset to update. + /// + public DomainId Id { get; set; } + + /// + /// The update type. + /// + public BulkUpdateAssetType Type { get; set; } + + /// + /// The parent folder id. + /// + public DomainId ParentId { get; set; } + + /// + /// The optional path to the folder. + /// + public string? ParentPath { get; set; } + + /// + /// The new name of the asset. + /// + public string? FileName { get; set; } + + /// + /// The new slug of the asset. + /// + public string? Slug { get; set; } + + /// + /// True, when the asset is not public. + /// + public bool? IsProtected { get; set; } + + /// + /// The new asset tags. + /// + public HashSet? Tags { get; set; } + + /// + /// The asset metadata. + /// + public AssetMetadata? Metadata { get; set; } + + /// + /// True to delete the asset permanently. + /// + public bool Permanent { get; set; } + + /// + /// The expected version. + /// + public long ExpectedVersion { get; set; } = EtagVersion.Any; + + public BulkUpdateJob ToJob() + { + return SimpleMapper.Map(this, new BulkUpdateJob()); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetDto.cs similarity index 71% rename from backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetDto.cs index e154da314..0a05c7580 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetDto.cs @@ -7,24 +7,25 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Assets.Models { - public sealed class MoveAssetItemDto + public sealed class MoveAssetDto { /// /// The parent folder id. /// public DomainId ParentId { get; set; } - public MoveAsset ToCommand(DomainId id) - { - return new MoveAsset { AssetId = id, ParentId = ParentId }; - } + /// + /// The optional path to the folder. + /// + public string? ParentPath { get; set; } - public MoveAssetFolder ToFolderCommand(DomainId id) + public MoveAsset ToCommand(DomainId id) { - return new MoveAssetFolder { AssetFolderId = id, ParentId = ParentId }; + return SimpleMapper.Map(this, new MoveAsset { AssetId = id }); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs new file mode 100644 index 000000000..75782d553 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class MoveAssetFolderDto + { + /// + /// The parent folder id. + /// + public DomainId ParentId { get; set; } + + public MoveAssetFolder ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new MoveAssetFolder { AssetFolderId = id }); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkResultDto.cs b/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs similarity index 66% rename from backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkResultDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs index 526fe8e4b..cc204e766 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkResultDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs @@ -5,13 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers { public sealed class BulkResultDto { @@ -26,15 +27,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public int JobIndex { get; set; } /// - /// The id of the content that has been handled successfully or not. + /// The id of the entity that has been handled successfully or not. /// - public DomainId? ContentId { get; set; } + public DomainId? Id { get; set; } - public static BulkResultDto FromImportResult(BulkUpdateResultItem result, HttpContext httpContext) + /// + /// The id of the entity that has been handled successfully or not. + /// + [Obsolete("Use Id instead.")] + public DomainId? ContentId => Id; + + public static BulkResultDto FromBulkResult(BulkUpdateResultItem result, HttpContext httpContext) { var error = result.Exception?.ToErrorDto(httpContext).Error; - return SimpleMapper.Map(result, new BulkResultDto { Error = error }); + return SimpleMapper.Map(result, new BulkResultDto { Error = error, Id = result.Id }); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 163a71a1b..d7f817cf4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -343,9 +344,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The full data for the content item. - /// True to automatically publish the content. - /// The optional custom content id. + /// The request parameters. /// /// 201 => Content created. /// 400 => Content request not valid. @@ -359,14 +358,9 @@ namespace Squidex.Areas.Api.Controllers.Contents [ProducesResponseType(typeof(ContentsDto), 201)] [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiCosts(1)] - public async Task PostContent(string app, string name, [FromBody] ContentData request, [FromQuery] bool publish = false, [FromQuery] DomainId? id = null) + public async Task PostContent(string app, string name, CreateContentDto request) { - var command = new CreateContent { Data = request, Publish = publish }; - - if (id != null && id.Value != default && !string.IsNullOrWhiteSpace(id.Value.ToString())) - { - command.ContentId = id.Value; - } + var command = request.ToCommand(); var response = await InvokeCommandAsync(command); @@ -380,7 +374,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// The name of the schema. /// The import request. /// - /// 201 => Contents created. + /// 200 => Contents created. /// 400 => Content request not valid. /// 404 => Content references, schema or app not found. /// @@ -392,6 +386,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiCosts(5)] + [Obsolete("Use bulk endpoint")] public async Task PostContents(string app, string name, [FromBody] ImportContentsDto request) { var command = request.ToCommand(); @@ -399,7 +394,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = result.Select(x => BulkResultDto.FromImportResult(x, HttpContext)).ToArray(); + var response = result.Select(x => BulkResultDto.FromBulkResult(x, HttpContext)).ToArray(); return Ok(response); } @@ -411,9 +406,9 @@ namespace Squidex.Areas.Api.Controllers.Contents /// The name of the schema. /// The bulk update request. /// - /// 201 => Contents created. - /// 400 => Content request not valid. - /// 404 => Content references, schema or app not found. + /// 201 => Contents created, update or delete. + /// 400 => Contents request not valid. + /// 404 => Contents references, schema or app not found. /// /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. @@ -423,14 +418,14 @@ namespace Squidex.Areas.Api.Controllers.Contents [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContents)] [ApiCosts(5)] - public async Task BulkContents(string app, string name, [FromBody] BulkUpdateDto request) + public async Task BulkUpdateContents(string app, string name, [FromBody] BulkUpdateContentsDto request) { var command = request.ToCommand(); var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = result.Select(x => BulkResultDto.FromImportResult(x, HttpContext)).ToArray(); + var response = result.Select(x => BulkResultDto.FromBulkResult(x, HttpContext)).ToArray(); return Ok(response); } @@ -441,10 +436,9 @@ namespace Squidex.Areas.Api.Controllers.Contents /// The name of the app. /// The name of the schema. /// The id of the content item to update. - /// True to automatically publish the content. - /// The full data for the content item. + /// The request parameters. /// - /// 200 => Content updated. + /// 200 => Content created or updated. /// 400 => Content request not valid. /// 404 => Content references, schema or app not found. /// @@ -456,9 +450,9 @@ namespace Squidex.Areas.Api.Controllers.Contents [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpsert)] [ApiCosts(1)] - public async Task PostContent(string app, string name, DomainId id, [FromBody] ContentData request, [FromQuery] bool publish = false) + public async Task PostUpsertContent(string app, string name, DomainId id, UpsertContentDto request) { - var command = new UpsertContent { ContentId = id, Data = request, Publish = publish }; + var command = request.ToCommand(id); var response = await InvokeCommandAsync(command); @@ -543,7 +537,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsChangeStatusOwn)] [ApiCosts(1)] - public async Task PutContentStatus(string app, string name, DomainId id, ChangeStatusDto request) + public async Task PutContentStatus(string app, string name, DomainId id, [FromBody] ChangeStatusDto request) { var command = request.ToCommand(id); @@ -613,6 +607,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// The name of the schema. /// The id of the content item to delete. /// True to check referrers of this content. + /// True to delete the asset permanently. /// /// 204 => Content deleted. /// 400 => Content cannot be deleted. @@ -625,9 +620,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [Route("content/{app}/{name}/{id}/")] [ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)] [ApiCosts(1)] - public async Task DeleteContent(string app, string name, DomainId id, [FromQuery] bool checkReferrers = false) + public async Task DeleteContent(string app, string name, DomainId id, + [FromQuery] bool checkReferrers = false, + [FromQuery] bool permanent = false) { - var command = new DeleteContent { ContentId = id, CheckReferrers = checkReferrers }; + var command = new DeleteContent { ContentId = id, CheckReferrers = checkReferrers, Permanent = permanent }; await CommandBus.PublishAsync(command); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs similarity index 64% rename from backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs index 7c83b07af..be789b0a9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs @@ -5,24 +5,29 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Linq; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; +#pragma warning disable CS0618 // Type or member is obsolete + namespace Squidex.Areas.Api.Controllers.Contents.Models { - public sealed class BulkUpdateDto + public sealed class BulkUpdateContentsDto { /// /// The contents to update or insert. /// [LocalizedRequired] - public BulkUpdateJobDto[]? Jobs { get; set; } + public BulkUpdateContentsJobDto[]? Jobs { get; set; } /// /// True to automatically publish the content. /// + [Obsolete("Use Jobs.Status")] public bool Publish { get; set; } /// @@ -30,6 +35,16 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// public bool DoNotScript { get; set; } = true; + /// + /// True to turn off validation for faster inserts. Default: false. + /// + public bool DoNotValidate { get; set; } + + /// + /// True to turn off validation of workflow rules. Default: false. + /// + public bool DoNotValidateWorkflow { get; set; } + /// /// True to check referrers of this content. /// @@ -46,6 +61,17 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray(); + if (result.Jobs != null && Publish) + { + foreach (var job in result.Jobs) + { + if (job != null) + { + job.Status = Status.Published; + } + } + } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs similarity index 87% rename from backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateJobDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs index 7e8886824..903552489 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Contents.Models { - public class BulkUpdateJobDto + public class BulkUpdateContentsJobDto { /// /// An optional query to identify the content to update. @@ -33,9 +33,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public ContentData? Data { get; set; } /// - /// The new status when the type is set to 'ChangeStatus'. + /// The new status when the type is set to 'ChangeStatus' or 'Upsert'. /// - public Status Status { get; set; } + public Status? Status { get; set; } /// /// The due time. @@ -45,13 +45,18 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// /// The update type. /// - public BulkUpdateType Type { get; set; } + public BulkUpdateContentType Type { get; set; } /// /// The optional schema id or name. /// public string? Schema { get; set; } + /// + /// True to delete the content permanently. + /// + public bool Permanent { get; set; } + /// /// The number of expected items. Set it to a higher number to update multiple items when a query is defined. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index bd20b5dc3..11e66614c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -149,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddGetLink("previous", resources.Url(x => nameof(x.GetContentVersion), versioned)); } - if (NewStatus.HasValue) + if (NewStatus != null) { if (resources.CanDeleteContentVersion(schema)) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs new file mode 100644 index 000000000..e85055eb2 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using StatusType = Squidex.Domain.Apps.Core.Contents.Status; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public class CreateContentDto + { + /// + /// The full data for the content item. + /// + [FromBody] + public ContentData Data { get; set; } + + /// + /// The initial status. + /// + [FromQuery] + public StatusType? Status { get; set; } + + /// + /// The optional custom content id. + /// + [FromQuery] + public DomainId? Id { get; set; } + + /// + /// True to automatically publish the content. + /// + [FromQuery] + [Obsolete("Use status query string.")] + public bool Publish { get; set; } + + public CreateContent ToCommand() + { + var command = new CreateContent { Data = Data! }; + + if (Id != null && Id.Value != default && !string.IsNullOrWhiteSpace(Id.Value.ToString())) + { + command.ContentId = Id.Value; + } + + if (Status != null) + { + command.Status = Status; + } + else if (Publish) + { + command.Status = StatusType.Published; + } + + return command; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs index 1803ba11f..36b488e76 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs @@ -5,12 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; +using System.Linq; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; +#pragma warning disable CS0618 // Type or member is obsolete + namespace Squidex.Areas.Api.Controllers.Contents.Models { public sealed class ImportContentsDto @@ -24,6 +28,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// /// True to automatically publish the content. /// + [Obsolete("Use Bulk endpoint")] public bool Publish { get; set; } /// @@ -38,7 +43,22 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public BulkUpdateContents ToCommand() { - return SimpleMapper.Map(this, new BulkUpdateContents()); + var result = SimpleMapper.Map(this, new BulkUpdateContents()); + + result.Jobs = Datas?.Select(x => new BulkUpdateJob { Type = BulkUpdateContentType.Create, Data = x }).ToArray(); + + if (result.Jobs != null && Publish) + { + foreach (var job in result.Jobs) + { + if (job != null) + { + job.Status = Status.Published; + } + } + } + + return result; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs new file mode 100644 index 000000000..d1686994f --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using StatusType = Squidex.Domain.Apps.Core.Contents.Status; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public class UpsertContentDto + { + /// + /// The full data for the content item. + /// + [FromBody] + public ContentData Data { get; set; } + + /// + /// The initial status. + /// + [FromQuery] + public StatusType? Status { get; set; } + + /// + /// True to automatically publish the content. + /// + [FromQuery] + [Obsolete("Use status query string.")] + public bool Publish { get; set; } + + public UpsertContent ToCommand(DomainId id) + { + var command = new UpsertContent { Data = Data!, ContentId = id }; + + if (Status != null) + { + command.Status = Status; + } + else if (Publish) + { + command.Status = StatusType.Published; + } + + return command; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index 2002534df..2ec7168b9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -80,7 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models AddPutLink("update", resources.Url(x => nameof(x.PutEvent), values)); - if (NextAttempt.HasValue) + if (NextAttempt != null) { AddDeleteLink("delete", resources.Url(x => nameof(x.DeleteEvent), values)); } diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs index bc47afaf5..5299cce05 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs @@ -41,7 +41,7 @@ namespace Squidex.Areas.Frontend.Middlewares public static string AdjustBase(this string html, HttpContext httpContext) { - if (httpContext.Request.PathBase.HasValue) + if (httpContext.Request.PathBase != null) { html = html.Replace("", $""); } diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 191ee1062..6a1b32f7b 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -65,6 +65,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index 35a5688ca..8971b6973 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -74,10 +74,13 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs() .As(); services.AddSingletonAs() @@ -112,8 +115,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - - services.AddSingleton(typeof(IEventEnricher<>), typeof(DefaultEventEnricher<>)); } } } \ No newline at end of file diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 7574332ed..1c84adb97 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -68,6 +68,7 @@ namespace Squidex.Config.Web services.Configure(options => { + options.SuppressInferBindingSourcesForParameters = true; options.SuppressModelStateInvalidFilter = true; }); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index 6a49d922e..5c1db4e66 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -148,7 +148,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting } [Fact] - public async Task TransformAsync_should_throw_when_script_failed() + public async Task TransformAsync_should_throw_exception_when_script_failed() { var content = new ContentData(); var context = new ScriptVars { Data = content }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index ad2a64703..4d1e8523b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Validation; using Xunit; @@ -22,6 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { public class AppCommandMiddlewareTests : HandlerTestBase { + private readonly IGrainFactory grainFactory = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly IAppImageStore appImageStore = A.Fake(); private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); @@ -35,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject protected override DomainId Id { - get { return appId.Id; } + get => appId.Id; } public AppCommandMiddlewareTests() @@ -45,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject A.CallTo(() => contextProvider.Context) .Returns(requestContext); - sut = new AppCommandMiddleware(A.Fake(), appImageStore, assetThumbnailGenerator, contextProvider); + sut = new AppCommandMiddleware(grainFactory, appImageStore, assetThumbnailGenerator, contextProvider); } [Fact] @@ -53,13 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var result = A.Fake(); - var context = - CreateCommandContext( - new MyCommand()); - - context.Complete(result); - - await sut.HandleAsync(context); + await HandleAsync(new UpdateApp(), result); Assert.Same(result, requestContext.App); } @@ -69,14 +65,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var file = new NoopAssetFile(); - var context = - CreateCommandContext( - new UploadAppImage { AppId = appId, File = file }); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._)) .Returns(new ImageInfo(100, 100, false)); - await sut.HandleAsync(context); + await HandleAsync(new UploadAppImage { File = file }, None.Value); A.CallTo(() => appImageStore.UploadAsync(appId.Id, A._, A._)) .MustHaveHappened(); @@ -85,18 +77,29 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject [Fact] public async Task Should_throw_exception_when_file_to_upload_is_not_an_image() { - var stream = new MemoryStream(); - var file = new NoopAssetFile(); - var context = - CreateCommandContext( - new UploadAppImage { AppId = appId, File = file }); + var command = new UploadAppImage { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._)) .Returns(Task.FromResult(null)); - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + await Assert.ThrowsAsync(() => HandleAsync(sut, command)); + } + + private Task HandleAsync(AppUpdateCommand command, object result) + { + command.AppId = appId; + + var grain = A.Fake(); + + A.CallTo(() => grain.ExecuteAsync(A>._)) + .Returns(new CommandResult(command.AggregateId, 1, 0, result)); + + A.CallTo(() => grainFactory.GetGrain(command.AggregateId.ToString(), null)) + .Returns(grain); + + return HandleAsync(sut, command); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs index 5bbb5b715..2f31e2916 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs @@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Json.Objects; using Squidex.Log; using Squidex.Shared.Users; @@ -44,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject protected override DomainId Id { - get { return AppId; } + get => AppId; } public AppDomainObjectTests() @@ -80,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteArchiveAsync(); - await Assert.ThrowsAsync(ExecuteAttachClientAsync); + await Assert.ThrowsAsync(ExecuteAttachClientAsync); } [Fact] @@ -212,7 +211,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(4)); + result.ShouldBeEquivalent(None.Value); Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); @@ -238,7 +237,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(5)); + result.ShouldBeEquivalent(None.Value); Assert.Null(sut.Snapshot.Plan); @@ -303,7 +302,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(4)); + result.ShouldBeEquivalent(None.Value); A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, A._)) .MustNotHaveHappened(); @@ -668,7 +667,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(4)); + result.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsArchived); @@ -731,26 +730,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return PublishAsync(new ArchiveApp()); } - private async Task PublishIdempotentAsync(AppCommand command) + private Task PublishIdempotentAsync(AppCommand command) { - var result = await PublishAsync(command); - - var previousSnapshot = sut.Snapshot; - var previousVersion = sut.Snapshot.Version; - - await PublishAsync(command); - - Assert.Same(previousSnapshot, sut.Snapshot); - Assert.Equal(previousVersion, sut.Snapshot.Version); - - return result; + return PublishIdempotentAsync(sut, CreateCommand(command)); } - private async Task PublishAsync(AppCommand command) + private async Task PublishAsync(AppCommand command) { var result = await sut.ExecuteAsync(CreateCommand(command)); - return result; + return result.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexIntegrationTests.cs index 701c844e7..1eb1511ac 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexIntegrationTests.cs @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes var appGrain = A.Fake(); A.CallTo(() => appGrain.GetStateAsync()) - .ReturnsLazily(() => CreateEntity().AsJ()); + .ReturnsLazily(() => CreateApp().AsJ()); A.CallTo(() => GrainFactory.GetGrain(AppId.Id.ToString(), null)) .Returns(appGrain); @@ -77,23 +77,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes .MustHaveHappenedANumberOfTimesMatching(x => x == count); } - private IAppEntity CreateEntity() + private IAppEntity CreateApp() { - var appEntity = A.Fake(); + var app = A.Fake(); - A.CallTo(() => appEntity.Id) + A.CallTo(() => app.Id) .Returns(AppId.Id); - A.CallTo(() => appEntity.Name) + A.CallTo(() => app.Name) .Returns(AppId.Name); - A.CallTo(() => appEntity.Version) + A.CallTo(() => app.Version) .Returns(version); - A.CallTo(() => appEntity.Contributors) + A.CallTo(() => app.Contributors) .Returns(new AppContributors(contributors.ToDictionary())); - return appEntity; + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs index 08040b8ab..249914d99 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_all_apps_from_user_permissions() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new[] { appId.Name }))) .Returns(new List { appId.Id }); @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_all_apps_from_user() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); A.CallTo(() => indexForUser.GetIdsAsync()) .Returns(new List { appId.Id }); @@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_combined_apps() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new[] { appId.Name }))) .Returns(new List { appId.Id }); @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_all_apps() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); A.CallTo(() => indexByName.GetIdsAsync()) .Returns(new List { appId.Id }); @@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_app_by_name() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); A.CallTo(() => indexByName.GetIdAsync(appId.Name)) .Returns(appId.Id); @@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_app_by_name_and_id_if_cached_before() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); A.CallTo(() => indexByName.GetIdAsync(appId.Name)) .Returns(appId.Id); @@ -158,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_app_by_id() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); var actual1 = await sut.GetAppAsync(appId.Id, false); var actual2 = await sut.GetAppAsync(appId.Id, false); @@ -176,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_resolve_app_by_id_and_name_if_cached_before() { - var (expected, _) = SetupApp(); + var (expected, _) = CreateApp(); var actual1 = await sut.GetAppAsync(appId.Id, true); var actual2 = await sut.GetAppAsync(appId.Id, true); @@ -196,7 +196,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_return_null_if_app_archived() { - SetupApp(isArchived: true); + CreateApp(isArchived: true); var actual1 = await sut.GetAppAsync(appId.Id, true); var actual2 = await sut.GetAppAsync(appId.Id, true); @@ -208,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_return_null_if_app_not_created() { - SetupApp(EtagVersion.NotFound); + CreateApp(EtagVersion.Empty); var actual1 = await sut.GetAppAsync(appId.Id, true); var actual2 = await sut.GetAppAsync(appId.Id, true); @@ -345,7 +345,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_add_app_to_index_when_contributor_assigned() { - SetupApp(); + CreateApp(); var command = new AssignContributor { AppId = appId, ContributorId = userId }; @@ -362,7 +362,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_update_index_when_app_is_updated() { - var (_, appGrain) = SetupApp(); + var (_, appGrain) = CreateApp(); var command = new UpdateApp { AppId = appId }; @@ -379,7 +379,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_update_index_with_result_when_app_is_updated() { - var (app, appGrain) = SetupApp(); + var (app, appGrain) = CreateApp(); var command = new UpdateApp { AppId = appId }; @@ -396,7 +396,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_remove_from_user_index_when_contributor_removed() { - SetupApp(); + CreateApp(); var command = new RemoveContributor { AppId = appId, ContributorId = userId }; @@ -413,7 +413,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_remove_app_from_indexes_when_app_gets_archived() { - SetupApp(isArchived: true); + CreateApp(isArchived: true); var command = new ArchiveApp { AppId = appId }; @@ -433,7 +433,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_also_remove_app_from_client_index_when_created_by_client() { - SetupApp(fromClient: true); + CreateApp(fromClient: true); var command = new ArchiveApp { AppId = appId }; @@ -513,41 +513,41 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes .MustHaveHappened(); } - private (IAppEntity, IAppGrain) SetupApp(long version = 0, bool fromClient = false, bool isArchived = false) + private (IAppEntity, IAppGrain) CreateApp(long version = 0, bool fromClient = false, bool isArchived = false) { - var appEntity = A.Fake(); + var app = A.Fake(); - A.CallTo(() => appEntity.Id) + A.CallTo(() => app.Id) .Returns(appId.Id); - A.CallTo(() => appEntity.Name) + A.CallTo(() => app.Name) .Returns(appId.Name); - A.CallTo(() => appEntity.Version) + A.CallTo(() => app.Version) .Returns(version); - A.CallTo(() => appEntity.IsArchived) + A.CallTo(() => app.IsArchived) .Returns(isArchived); - A.CallTo(() => appEntity.Contributors) + A.CallTo(() => app.Contributors) .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); if (fromClient) { - A.CallTo(() => appEntity.CreatedBy) + A.CallTo(() => app.CreatedBy) .Returns(ClientActor()); } else { - A.CallTo(() => appEntity.CreatedBy) + A.CallTo(() => app.CreatedBy) .Returns(UserActor()); } var appGrain = A.Fake(); A.CallTo(() => appGrain.GetStateAsync()) - .Returns(J.Of(appEntity)); + .Returns(J.Of(app)); A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) .Returns(appGrain); - return (appEntity, appGrain); + return (app, appGrain); } private CreateApp Create(string name) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs index 70d70f61d..fbf3b83a2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs @@ -5,22 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FakeItEasy; -using FluentAssertions; using Orleans; using Squidex.Assets; -using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Reflection; -using Squidex.Log; +using Squidex.Infrastructure.Orleans; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets.DomainObject @@ -29,16 +23,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { private readonly IAssetEnricher assetEnricher = A.Fake(); private readonly IAssetFileStore assetFileStore = A.Fake(); + private readonly IAssetFolderResolver assetFolderResolver = A.Fake(); private readonly IAssetMetadataSource assetMetadataSource = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IContentRepository contentRepository = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IServiceProvider serviceProvider = A.Fake(); - private readonly ITagService tagService = A.Fake(); private readonly DomainId assetId = DomainId.NewGuid(); - private readonly AssetDomainObjectGrain asset; - private readonly AssetFile file; + private readonly AssetFile file = new NoopAssetFile(); private readonly Context requestContext; private readonly AssetCommandMiddleware sut; @@ -48,38 +39,25 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject protected override DomainId Id { - get { return DomainId.Combine(AppId, assetId); } + get => DomainId.Combine(AppId, assetId); } public AssetCommandMiddlewareTests() { file = new NoopAssetFile(); - var assetDomainObject = new AssetDomainObject(Store, A.Dummy(), tagService, assetQuery, contentRepository); - - A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject))) - .Returns(assetDomainObject); - - asset = new AssetDomainObjectGrain(serviceProvider, null!); - asset.ActivateAsync(Id.ToString()).Wait(); - requestContext = Context.Anonymous(Mocks.App(AppNamedId)); A.CallTo(() => contextProvider.Context) .Returns(requestContext); - A.CallTo(() => assetEnricher.EnrichAsync(A._, requestContext)) - .ReturnsLazily(() => SimpleMapper.Map(asset.Snapshot, new AssetEntity())); - A.CallTo(() => assetQuery.FindByHashAsync(A._, A._, A._, A._)) .Returns(Task.FromResult(null)); - A.CallTo(() => grainFactory.GetGrain(Id.ToString(), null)) - .Returns(asset); - sut = new AssetCommandMiddleware(grainFactory, assetEnricher, assetFileStore, + assetFolderResolver, assetQuery, contextProvider, new[] { assetMetadataSource }); } @@ -87,13 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject [Fact] public async Task Should_not_invoke_enricher_for_other_result() { - var context = - CreateCommandContext( - new MyCommand()); - - context.Complete(12); - - await sut.HandleAsync(context); + await HandleAsync(new AnnotateAsset(), 12); A.CallTo(() => assetEnricher.EnrichAsync(A._, requestContext)) .MustNotHaveHappened(); @@ -105,14 +77,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var result = new AssetEntity(); var context = - CreateCommandContext( - new MyCommand()); - - context.Complete(result); - - await sut.HandleAsync(context); - - Assert.Same(result, context.Result()); + await HandleAsync(new AnnotateAsset(), + result); A.CallTo(() => assetEnricher.EnrichAsync(A._, requestContext)) .MustNotHaveHappened(); @@ -123,34 +89,28 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { var result = A.Fake(); - var context = - CreateCommandContext( - new MyCommand()); - - context.Complete(result); - var enriched = new AssetEntity(); A.CallTo(() => assetEnricher.EnrichAsync(result, requestContext)) .Returns(enriched); - await sut.HandleAsync(context); + var context = + await HandleAsync(new AnnotateAsset(), + result); Assert.Same(enriched, context.Result()); } [Fact] - public async Task Create_should_create_domain_object() + public async Task Create_should_upload_file() { - var context = - CreateCommandContext( - new CreateAsset { AssetId = assetId, File = file }); - - await sut.HandleAsync(context); + var result = CreateAsset(); - var result = context.Result(); + var context = + await HandleAsync(new CreateAsset { File = file }, + result); - result.Asset.Should().BeEquivalentTo(asset.Snapshot, x => x.ExcludingMissingMembers()); + Assert.Same(result, context.Result()); AssertAssetHasBeenUploaded(0); AssertMetadataEnriched(); @@ -159,77 +119,58 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject [Fact] public async Task Create_should_calculate_hash() { - var command = new CreateAsset { AssetId = assetId, File = file }; + var command = new CreateAsset { File = file }; - var context = - CreateCommandContext( - command); - - await sut.HandleAsync(context); + await HandleAsync(command, CreateAsset()); Assert.True(command.FileHash.Length > 10); } [Fact] - public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() + public async Task Create_should_resolve_path() { - var context = - CreateCommandContext( - new CreateAsset { AssetId = assetId, File = file }); + var folderId = DomainId.NewGuid(); - SetupSameHashAsset(file.FileName, file.FileSize, out _); + var command = new CreateAsset { File = file, ParentPath = "path/to/folder" }; - await sut.HandleAsync(context); + A.CallTo(() => assetFolderResolver.ResolveOrCreateAsync(requestContext, A._, "path/to/folder")) + .Returns(folderId); - var result = context.Result(); + await HandleAsync(command, CreateAsset()); - Assert.True(result.IsDuplicate); + Assert.Equal(folderId, command.ParentId); } [Fact] public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_found_but_duplicate_allowed() { - var context = - CreateCommandContext( - new CreateAsset { AssetId = assetId, File = file, Duplicate = true }); + var result = CreateAsset(); SetupSameHashAsset(file.FileName, file.FileSize, out _); - await sut.HandleAsync(context); - - var result = context.Result(); + var context = + await HandleAsync(new CreateAsset { File = file, Duplicate = true }, + result); - Assert.False(result.IsDuplicate); + Assert.Same(result, context.Result()); } [Fact] - public async Task Create_should_pass_through_duplicate() + public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() { - var context = - CreateCommandContext( - new CreateAsset { AssetId = assetId, File = file }); - SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate); - await sut.HandleAsync(context); - - var result = context.Result(); - - Assert.True(result.IsDuplicate); + var context = + await HandleAsync(new CreateAsset { File = file }, + CreateAsset()); - result.Should().BeEquivalentTo(duplicate, x => x.ExcludingMissingMembers()); + Assert.Same(duplicate, context.Result().Asset); } [Fact] - public async Task Update_should_update_domain_object() + public async Task Update_should_upload_file() { - var context = - CreateCommandContext( - new UpdateAsset { AssetId = assetId, File = file }); - - await ExecuteCreateAsync(); - - await sut.HandleAsync(context); + await HandleAsync(new UpdateAsset { File = file }, CreateAsset(1)); AssertAssetHasBeenUploaded(1); AssertMetadataEnriched(); @@ -238,63 +179,67 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject [Fact] public async Task Update_should_calculate_hash() { - var command = new UpdateAsset { AssetId = assetId, File = file }; - - var context = - CreateCommandContext( - command); + var command = new UpdateAsset { File = file }; - await ExecuteCreateAsync(); - - await sut.HandleAsync(context); + await HandleAsync(command, CreateAsset()); Assert.True(command.FileHash.Length > 10); } [Fact] - public async Task Update_should_enrich_asset() + public async Task Upsert_should_upload_file() { - var context = - CreateCommandContext( - new UpdateAsset { AssetId = assetId, File = file }); + await HandleAsync(new UpsertAsset { File = file }, CreateAsset(1)); - await ExecuteCreateAsync(); + AssertAssetHasBeenUploaded(1); + AssertMetadataEnriched(); + } - await sut.HandleAsync(context); + [Fact] + public async Task Upsert_should_calculate_hash() + { + var command = new UpsertAsset { File = file }; - var result = context.Result(); + await HandleAsync(command, CreateAsset()); - result.Should().BeEquivalentTo(asset.Snapshot, x => x.ExcludingMissingMembers()); + Assert.True(command.FileHash.Length > 10); } [Fact] - public async Task AnnotateAsset_should_enrich_asset() + public async Task Upsert_should_resolve_path() { - var context = - CreateCommandContext( - new AnnotateAsset { AssetId = assetId, FileName = "newName" }); + var folderId = DomainId.NewGuid(); - await ExecuteCreateAsync(); + var command = new UpsertAsset { File = file, ParentPath = "path/to/folder" }; - await sut.HandleAsync(context); + A.CallTo(() => assetFolderResolver.ResolveOrCreateAsync(requestContext, A._, "path/to/folder")) + .Returns(folderId); - var result = context.Result(); + await HandleAsync(command, CreateAsset()); - result.Should().BeEquivalentTo(asset.Snapshot, x => x.ExcludingMissingMembers()); + Assert.Equal(folderId, command.ParentId); } - private Task ExecuteCreateAsync() + [Fact] + public async Task Move_should_resolve_path() { - var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file }); + var folderId = DomainId.NewGuid(); + + var command = new MoveAsset { ParentPath = "path/to/folder" }; + + A.CallTo(() => assetFolderResolver.ResolveOrCreateAsync(requestContext, A._, "path/to/folder")) + .Returns(folderId); - return asset.ExecuteAsync(CommandRequest.Create(command)); + await HandleAsync(command, CreateAsset()); + + Assert.Equal(folderId, command.ParentId); } - private void AssertAssetHasBeenUploaded(long version) + private void AssertAssetHasBeenUploaded(long fileVersion) { A.CallTo(() => assetFileStore.UploadAsync(A._, A._, CancellationToken.None)) .MustHaveHappened(); - A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, version, CancellationToken.None)) + A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, fileVersion, CancellationToken.None)) .MustHaveHappened(); A.CallTo(() => assetFileStore.DeleteAsync(A._)) .MustHaveHappened(); @@ -314,8 +259,30 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private void AssertMetadataEnriched() { - A.CallTo(() => assetMetadataSource.EnhanceAsync(A._, A>._)) + A.CallTo(() => assetMetadataSource.EnhanceAsync(A._)) .MustHaveHappened(); } + + private Task HandleAsync(AssetCommand command, object result) + { + command.AssetId = assetId; + + CreateCommand(command); + + var grain = A.Fake(); + + A.CallTo(() => grain.ExecuteAsync(A>._)) + .Returns(new CommandResult(command.AggregateId, 1, 0, result)); + + A.CallTo(() => grainFactory.GetGrain(command.AggregateId.ToString(), null)) + .Returns(grain); + + return HandleAsync(sut, command); + } + + private IAssetEntity CreateAsset(long fileVersion = 0) + { + return new AssetEntity { AppId = AppNamedId, Id = assetId, FileVersion = fileVersion }; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs index dc558757d..220db0305 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs @@ -18,7 +18,6 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Log; using Xunit; @@ -36,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject protected override DomainId Id { - get { return assetId; } + get => assetId; } public AssetDomainObjectTests() @@ -45,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject .Returns(new List { A.Fake() }); A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>._, A>._)) - .ReturnsLazily(x => Task.FromResult(x.GetArgument>(2)?.ToDictionary(x => x)!)); + .ReturnsLazily(x => Task.FromResult(x.GetArgument>(2)?.ToDictionary(x => x) ?? new Dictionary())); sut = new AssetDomainObject(Store, A.Dummy(), tagService, assetQuery, contentRepository); sut.Setup(Id); @@ -57,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); await ExecuteDeleteAsync(); - await Assert.ThrowsAsync(ExecuteUpdateAsync); + await Assert.ThrowsAsync(ExecuteUpdateAsync); } [Fact] @@ -88,6 +87,83 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject ); } + [Fact] + public async Task Create_should_recreate_deleted_content() + { + var command = new CreateAsset { File = file, FileHash = "NewHash" }; + + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await PublishAsync(command); + } + + [Fact] + public async Task Create_should_recreate_permanently_deleted_content() + { + var command = new CreateAsset { File = file, FileHash = "NewHash" }; + + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(true); + + await PublishAsync(command); + } + + [Fact] + public async Task Upsert_should_create_events_and_set_intitial_state_when_not_found() + { + var command = new UpsertAsset { File = file, FileHash = "NewHash" }; + + var result = await PublishAsync(command); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(0, sut.Snapshot.FileVersion); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetCreated + { + FileName = file.FileName, + FileSize = file.FileSize, + FileHash = command.FileHash, + FileVersion = 0, + Metadata = new AssetMetadata(), + MimeType = file.MimeType, + Tags = new HashSet(), + Slug = file.FileName.ToAssetSlug() + }) + ); + } + + [Fact] + public async Task Upsert_should_create_events_and_update_file_state_when_found() + { + var command = new UpsertAsset { File = file, FileHash = "NewHash" }; + + await ExecuteCreateAsync(); + + var result = await PublishAsync(command); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(1, sut.Snapshot.FileVersion); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetUpdated + { + FileSize = file.FileSize, + FileHash = command.FileHash, + FileVersion = 1, + Metadata = new AssetMetadata(), + MimeType = file.MimeType + }) + ); + } + [Fact] public async Task Update_should_create_events_and_update_file_state() { @@ -237,7 +313,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(2)); + result.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -247,6 +323,24 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject ); } + [Fact] + public async Task Delete_should_not_create_events_if_permanent() + { + var command = new DeleteAsset { Permanent = true }; + + await ExecuteCreateAsync(); + + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All)) + .Returns(true); + + var result = await PublishAsync(command); + + result.ShouldBeEquivalent(None.Value); + + Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); + Assert.Empty(LastEvents); + } + [Fact] public async Task Delete_should_throw_exception_if_referenced_by_other_item() { @@ -275,53 +369,43 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private Task ExecuteCreateAsync() { - return PublishAsync(new CreateAsset { File = file }); + return PublishAsync(new CreateAsset { File = file, FileHash = "123" }); } private Task ExecuteUpdateAsync() { - return PublishAsync(new UpdateAsset { File = file }); + return PublishAsync(new UpdateAsset { File = file, FileHash = "456" }); } - private Task ExecuteDeleteAsync() + private Task ExecuteDeleteAsync(bool permanent = false) { - return PublishAsync(new DeleteAsset()); + return PublishAsync(new DeleteAsset { Permanent = permanent }); } - protected T CreateAssetEvent(T @event) where T : AssetEvent + private T CreateAssetEvent(T @event) where T : AssetEvent { @event.AssetId = assetId; return CreateEvent(@event); } - protected T CreateAssetCommand(T command) where T : AssetCommand + private T CreateAssetCommand(T command) where T : AssetCommand { command.AssetId = assetId; return CreateCommand(command); } - private async Task PublishIdempotentAsync(AssetCommand command) + private Task PublishIdempotentAsync(AssetCommand command) { - var result = await PublishAsync(command); - - var previousSnapshot = sut.Snapshot; - var previousVersion = sut.Snapshot.Version; - - await PublishAsync(command); - - Assert.Same(previousSnapshot, sut.Snapshot); - Assert.Equal(previousVersion, sut.Snapshot.Version); - - return result; + return PublishIdempotentAsync(sut, CreateAssetCommand(command)); } - private async Task PublishAsync(AssetCommand command) + private async Task PublishAsync(AssetCommand command) { var result = await sut.ExecuteAsync(CreateAssetCommand(command)); - return result; + return result.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs index b448f8d86..894bf56d1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Log; using Xunit; @@ -27,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject protected override DomainId Id { - get { return DomainId.Combine(AppId, assetFolderId); } + get => DomainId.Combine(AppId, assetFolderId); } public AssetFolderDomainObjectTests() @@ -45,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); await ExecuteDeleteAsync(); - await Assert.ThrowsAsync(ExecuteUpdateAsync); + await Assert.ThrowsAsync(ExecuteUpdateAsync); } [Fact] @@ -112,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -137,40 +136,30 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject return PublishAsync(new DeleteAssetFolder()); } - protected T CreateAssetFolderEvent(T @event) where T : AssetFolderEvent + private T CreateAssetFolderEvent(T @event) where T : AssetFolderEvent { @event.AssetFolderId = assetFolderId; return CreateEvent(@event); } - protected T CreateAssetFolderCommand(T command) where T : AssetFolderCommand + private T CreateAssetFolderCommand(T command) where T : AssetFolderCommand { command.AssetFolderId = assetFolderId; return CreateCommand(command); } - private async Task PublishIdempotentAsync(AssetFolderCommand command) + private Task PublishIdempotentAsync(AssetFolderCommand command) { - var result = await PublishAsync(command); - - var previousSnapshot = sut.Snapshot; - var previousVersion = sut.Snapshot.Version; - - await PublishAsync(command); - - Assert.Same(previousSnapshot, sut.Snapshot); - Assert.Equal(previousVersion, sut.Snapshot.Version); - - return result; + return PublishIdempotentAsync(sut, CreateAssetFolderCommand(command)); } - private async Task PublishAsync(AssetFolderCommand command) + private async Task PublishAsync(AssetFolderCommand command) { var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); - return result; + return result.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderResolverTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderResolverTests.cs new file mode 100644 index 000000000..009328820 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderResolverTests.cs @@ -0,0 +1,165 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Caching; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +{ + public class AssetFolderResolverTests + { + private readonly ILocalCache localCache = new AsyncLocalCache(); + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly AssetFolderResolver sut; + + public AssetFolderResolverTests() + { + requestContext = Context.Anonymous(Mocks.App(appId)); + + localCache.StartContext(); + + sut = new AssetFolderResolver(localCache, assetQuery); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("/")] + [InlineData("\\")] + public async Task Should_resolve_root_id_for_empty_path(string path) + { + var folderId = await sut.ResolveOrCreateAsync(requestContext, commandBus, path); + + Assert.Equal(DomainId.Empty, folderId); + } + + [Fact] + public async Task Should_create_and_cache_level1_folder() + { + var folderId11_1 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1"); + var folderId11_2 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1"); + + Assert.NotEqual(DomainId.Empty, folderId11_1); + Assert.NotEqual(DomainId.Empty, folderId11_2); + + Assert.Equal(folderId11_2, folderId11_1); + + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.FolderName == "level1"))) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_create_and_cache_recursively() + { + var folderId21_1 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); + var folderId21_2 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); + + Assert.NotEqual(DomainId.Empty, folderId21_1); + Assert.NotEqual(DomainId.Empty, folderId21_2); + + Assert.Equal(folderId21_1, folderId21_2); + + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.FolderName == "level1"))) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.FolderName == "level2"))) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_cache_folders_on_same_level() + { + var folder11 = CreateFolder("level1_1"); + var folder12 = CreateFolder("level1_2"); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) + .Returns(ResultList.CreateFrom(2, folder11, folder12)); + + var folderId11 = await sut.ResolveOrCreateAsync(requestContext, commandBus, folder11.FolderName); + var folderId12 = await sut.ResolveOrCreateAsync(requestContext, commandBus, folder12.FolderName); + + Assert.Equal(folder11.Id, folderId11); + Assert.Equal(folder12.Id, folderId12); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => commandBus.PublishAsync(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_resolve_recursively() + { + var folder11 = CreateFolder("level1"); + var folder21 = CreateFolder("level2"); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) + .Returns(ResultList.CreateFrom(1, folder11)); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, folder11.Id)) + .Returns(ResultList.CreateFrom(1, folder21)); + + var folderId2 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); + + Assert.Equal(folder21.Id, folderId2); + + A.CallTo(() => commandBus.PublishAsync(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_resolve_recursively_and_create_folder() + { + var folder11 = CreateFolder("level1"); + var folder21 = CreateFolder("level2"); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) + .Returns(ResultList.CreateFrom(1, folder11)); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, folder11.Id)) + .Returns(ResultList.CreateFrom(1, folder21)); + + await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); + + var folderId3 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2/level3"); + + Assert.NotEqual(DomainId.Empty, folderId3); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, folder11.Id)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.FolderName == "level3" && x.ParentId == folder21.Id))) + .MustHaveHappenedOnceExactly(); + } + + private static IAssetFolderEntity CreateFolder(string name) + { + var assetFolder = A.Fake(); + + A.CallTo(() => assetFolder.FolderName) + .Returns(name); + + A.CallTo(() => assetFolder.Id) + .Returns(DomainId.NewGuid()); + + return assetFolder; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs new file mode 100644 index 000000000..03108f612 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs @@ -0,0 +1,208 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +{ + public class AssetsBulkUpdateCommandMiddlewareTests + { + private readonly IContextProvider contextProvider = A.Fake(); + private readonly ICommandBus commandBus = A.Dummy(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly AssetsBulkUpdateCommandMiddleware sut; + + public AssetsBulkUpdateCommandMiddlewareTests() + { + sut = new AssetsBulkUpdateCommandMiddleware(contextProvider); + } + + [Fact] + public async Task Should_do_nothing_if_jobs_is_null() + { + var command = new BulkUpdateAssets(); + + var result = await PublishAsync(command); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_do_nothing_if_jobs_is_empty() + { + var command = new BulkUpdateAssets { Jobs = Array.Empty() }; + + var result = await PublishAsync(command); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_annotate_asset() + { + SetupContext(Permissions.AppAssetsUpdate); + + var id = DomainId.NewGuid(); + + var command = BulkCommand(BulkUpdateAssetType.Annotate, id, fileName: "file"); + + var result = await PublishAsync(command); + + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id && x.FileName == "file"))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_throw_security_exception_when_user_has_no_permission_for_annotating() + { + SetupContext(Permissions.AppAssetsRead); + + var id = DomainId.NewGuid(); + + var command = BulkCommand(BulkUpdateAssetType.Move, id); + + var result = await PublishAsync(command); + + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + + A.CallTo(() => commandBus.PublishAsync(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_move_asset() + { + SetupContext(Permissions.AppAssetsUpdate); + + var id = DomainId.NewGuid(); + + var command = BulkCommand(BulkUpdateAssetType.Move, id, parentPath: "/path/to/folder"); + + var result = await PublishAsync(command); + + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id && x.ParentPath == "/path/to/folder"))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_throw_security_exception_when_user_has_no_permission_for_moving() + { + SetupContext(Permissions.AppAssetsRead); + + var id = DomainId.NewGuid(); + + var command = BulkCommand(BulkUpdateAssetType.Move, id); + + var result = await PublishAsync(command); + + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + + A.CallTo(() => commandBus.PublishAsync(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_delete_asset() + { + SetupContext(Permissions.AppAssetsDelete); + + var id = DomainId.NewGuid(); + + var command = BulkCommand(BulkUpdateAssetType.Delete, id); + + var result = await PublishAsync(command); + + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => x.AssetId == id))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_throw_security_exception_when_user_has_no_permission_for_deletion() + { + SetupContext(Permissions.AppAssetsRead); + + var id = DomainId.NewGuid(); + + var command = BulkCommand(BulkUpdateAssetType.Delete, id: id); + + var result = await PublishAsync(command); + + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + + A.CallTo(() => commandBus.PublishAsync(A._)) + .MustNotHaveHappened(); + } + + private async Task PublishAsync(ICommand command) + { + var context = new CommandContext(command, commandBus); + + await sut.HandleAsync(context); + + return (context.PlainResult as BulkUpdateResult)!; + } + + private BulkUpdateAssets BulkCommand(BulkUpdateAssetType type, DomainId id, + string? parentPath = null, string? fileName = null) + { + return new BulkUpdateAssets + { + AppId = appId, + Jobs = new[] + { + new BulkUpdateJob + { + Type = type, + Id = id, + ParentPath = parentPath, + FileName = fileName + } + } + }; + } + + private Context SetupContext(string id) + { + var permission = Permissions.ForApp(id, appId.Name).Id; + + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); + + var requestContext = new Context(claimsPrincipal, Mocks.App(appId)); + + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); + + return requestContext; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs index 3a8aa2c60..78315e597 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs @@ -26,34 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); [Fact] - public async Task CanCreate_should_not_throw_exception_when_folder_found() - { - var command = new CreateAsset { AppId = appId, ParentId = DomainId.NewGuid() }; - - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId)) - .Returns(new List { AssetFolder() }); - - await GuardAsset.CanCreate(command, assetQuery); - } - - [Fact] - public async Task CanCreate_should_throw_exception_when_folder_not_found() - { - var command = new CreateAsset { AppId = appId, ParentId = DomainId.NewGuid() }; - - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId)) - .Returns(new List()); - - await ValidationAssert.ThrowsAsync(() => GuardAsset.CanCreate(command, assetQuery), - new ValidationError("Asset folder does not exist.", "ParentId")); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_when_added_to_root() + public void CanCreate_should_not_throw_exception_when_added_to_root() { var command = new CreateAsset { AppId = appId }; - await GuardAsset.CanCreate(command, assetQuery); + GuardAsset.CanCreate(command); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs index 9af7b1db2..1469c9c95 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = FakeCommand("NoExtension"); - await sut.EnhanceAsync(command, null); + await sut.EnhanceAsync(command); Assert.Equal(AssetType.Unknown, command.Type); } @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = Command("SamplePNGImage_100kbmb.png"); - await sut.EnhanceAsync(command, null); + await sut.EnhanceAsync(command); Assert.Equal(AssetType.Image, command.Type); @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = Command("SampleAudio_0.4mb.mp3"); - await sut.EnhanceAsync(command, null); + await sut.EnhanceAsync(command); Assert.Equal(AssetType.Audio, command.Type); @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = Command("SampleVideo_1280x720_1mb.mp4"); - await sut.EnhanceAsync(command, null); + await sut.EnhanceAsync(command); Assert.Equal(AssetType.Video, command.Type); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs index 88f4e302f..b5358a309 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -15,7 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class FileTypeAssetMetadataSourceTests { - private readonly HashSet tags = new HashSet(); private readonly FileTypeAssetMetadataSource sut = new FileTypeAssetMetadataSource(); [Fact] @@ -23,9 +21,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset(); - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); - Assert.Empty(tags); + Assert.Empty(command.Tags); } [Fact] @@ -36,9 +34,9 @@ namespace Squidex.Domain.Apps.Entities.Assets File = new NoopAssetFile("File.DOCX") }; - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); - Assert.Contains("type/docx", tags); + Assert.Contains("type/docx", command.Tags); } [Fact] @@ -49,9 +47,9 @@ namespace Squidex.Domain.Apps.Entities.Assets File = new NoopAssetFile("File") }; - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); - Assert.Contains("type/blob", tags); + Assert.Contains("type/blob", command.Tags); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 32b867ec7..56e9e55bf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using FakeItEasy; @@ -20,7 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Assets public class ImageAssetMetadataSourceTests { private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); - private readonly HashSet tags = new HashSet(); private readonly MemoryStream stream = new MemoryStream(); private readonly AssetFile file; private readonly ImageAssetMetadataSource sut; @@ -37,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { File = file, Type = AssetType.Image }; - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._)) .MustHaveHappened(); @@ -48,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { File = file }; - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); - Assert.Empty(tags); + Assert.Empty(command.Tags); } [Fact] @@ -61,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) .Returns(new ImageInfo(800, 600, false)); - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); Assert.Equal(800, command.Metadata.GetPixelWidth()); Assert.Equal(600, command.Metadata.GetPixelHeight()); @@ -82,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, A._)) .Returns(new ImageInfo(800, 600, true)); - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); Assert.Equal(800, command.Metadata.GetPixelWidth()); Assert.Equal(600, command.Metadata.GetPixelHeight()); @@ -100,10 +98,10 @@ namespace Squidex.Domain.Apps.Entities.Assets command.Metadata.SetPixelWidth(100); command.Metadata.SetPixelWidth(100); - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); - Assert.Contains("image", tags); - Assert.Contains("image/small", tags); + Assert.Contains("image", command.Tags); + Assert.Contains("image/small", command.Tags); } [Fact] @@ -114,10 +112,10 @@ namespace Squidex.Domain.Apps.Entities.Assets command.Metadata.SetPixelWidth(800); command.Metadata.SetPixelWidth(600); - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); - Assert.Contains("image", tags); - Assert.Contains("image/medium", tags); + Assert.Contains("image", command.Tags); + Assert.Contains("image/medium", command.Tags); } [Fact] @@ -128,10 +126,10 @@ namespace Squidex.Domain.Apps.Entities.Assets command.Metadata.SetPixelWidth(1200); command.Metadata.SetPixelWidth(1400); - await sut.EnhanceAsync(command, tags); + await sut.EnhanceAsync(command); - Assert.Contains("image", tags); - Assert.Contains("image/large", tags); + Assert.Contains("image", command.Tags); + Assert.Contains("image/large", command.Tags); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs index 3874a4d2d..31d67437d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs @@ -40,17 +40,15 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject public async Task Should_invoke_grain_for_comments_command() { var command = CreateCommentsCommand(new CreateComment()); - var context = CreateContextForCommand(command); + var context = CrateCommandContext(command); var grain = A.Fake(); - var result = "Completed"; - A.CallTo(() => grainFactory.GetGrain(commentsId.ToString(), null)) .Returns(grain); A.CallTo(() => grain.ExecuteAsync(A>.That.Matches(x => x.Value == command))) - .Returns(new J(result)); + .Returns(new CommandResult(commentsId, 0, 0).AsJ()); var isNextCalled = false; @@ -62,9 +60,6 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject }); Assert.True(isNextCalled); - - A.CallTo(() => grain.ExecuteAsync(A>.That.Matches(x => x.Value == command))) - .Returns(new J(12)); } [Fact] @@ -78,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject Text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io" }); - var context = CreateContextForCommand(command); + var context = CrateCommandContext(command); await sut.HandleAsync(context); @@ -96,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject Text = "Hi @mail1@squidex.io and @mail2@squidex.io" }); - var context = CreateContextForCommand(command); + var context = CrateCommandContext(command); await sut.HandleAsync(context); @@ -112,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject Text = "Hi invalid@squidex.io" }); - var context = CreateContextForCommand(command); + var context = CrateCommandContext(command); await sut.HandleAsync(context); @@ -128,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject Text = "Hi @invalid@squidex.io", IsMention = true }; - var context = CreateContextForCommand(command); + var context = CrateCommandContext(command); await sut.HandleAsync(context); @@ -136,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject .MustNotHaveHappened(); } - protected CommandContext CreateContextForCommand(TCommand command) where TCommand : CommentsCommand + private CommandContext CrateCommandContext(ICommand command) { return new CommandContext(command, commandBus); } @@ -152,7 +147,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject .Returns(user); } - protected T CreateCommentsCommand(T command) where T : CommentsCommand + private T CreateCommentsCommand(T command) where T : CommentsCommand { command.Actor = actor; command.AppId = appId; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs index 01923041d..d9b8ec6eb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject private string Id { - get { return commentsId.ToString(); } + get => commentsId.ToString(); } public IEnumerable> LastEvents { get; private set; } = Enumerable.Empty>(); @@ -55,9 +55,13 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var result = await sut.ExecuteAsync(CreateCommentsCommand(command)); - result.ShouldBeEquivalent((object)EntityCreatedResult.Create(command.CommentId, 0)); + result.Value.ShouldBeEquivalent(new CommandResult(commentsId, 0, EtagVersion.Empty)); + + sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult + { + Version = 0 + }); - sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult { Version = 0 }); sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { CreatedComments = new List @@ -82,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand)); - result.ShouldBeEquivalent((object)new EntitySavedResult(1)); + result.Value.ShouldBeEquivalent(new CommandResult(commentsId, 1, 0)); sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { @@ -118,9 +122,13 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand)); - result.ShouldBeEquivalent((object)new EntitySavedResult(2)); + result.Value.ShouldBeEquivalent(new CommandResult(commentsId, 2, 1)); + + sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult + { + Version = 2 + }); - sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { Version = 2 }); sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult { DeletedComments = new List @@ -129,6 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject }, Version = 2 }); + sut.GetCommentsAsync(1).Result.Should().BeEquivalentTo(new CommentsResult { DeletedComments = new List @@ -154,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject return sut.ExecuteAsync(CreateCommentsCommand(new UpdateComment { Text = "text2" })); } - protected T CreateCommentsEvent(T @event) where T : CommentsEvent + private T CreateCommentsEvent(T @event) where T : CommentsEvent { @event.Actor = actor; @event.CommentsId = commentsId; @@ -163,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject return @event; } - protected T CreateCommentsCommand(T command) where T : CommentsCommand + private T CreateCommentsCommand(T command) where T : CommentsCommand { command.Actor = actor; command.CommentsId = commentsId; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 1ddddda43..6aa8035be 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -119,6 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task Should_create_enriched_events(ContentEvent @event, EnrichedContentEventType type) { @event.AppId = appId; + @event.SchemaId = schemaMatch; var envelope = Envelope.Create(@event).SetEventStreamNumber(12); @@ -135,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_enrich_with_old_data_when_updated() { - var @event = new ContentUpdated { AppId = appId, ContentId = DomainId.NewGuid() }; + var @event = new ContentUpdated { AppId = appId, ContentId = DomainId.NewGuid(), SchemaId = schemaMatch }; var envelope = Envelope.Create(@event).SetEventStreamNumber(12); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index 33960c8bd..d52da1b58 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -17,11 +17,19 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); [Fact] - public async Task Should_always_allow_publish_on_create() + public async Task Should_return_info_for_valid_status() { - var result = await sut.CanPublishOnCreateAsync(null!, null!, null!); + var info = await sut.GetInfoAsync(null!, Status.Draft); - Assert.True(result); + Assert.Equal(new StatusInfo(Status.Draft, StatusColors.Draft), info); + } + + [Fact] + public async Task Should_return_info_as_null_for_invalid_status() + { + var info = await sut.GetInfoAsync(null!, new Status("Invalid")); + + Assert.Null(info); } [Fact] @@ -33,7 +41,17 @@ namespace Squidex.Domain.Apps.Entities.Contents } [Fact] - public async Task Should_check_is_valid_next() + public async Task Should_allow_if_transition_is_valid() + { + var content = new ContentEntity { Status = Status.Published }; + + var result = await sut.CanMoveToAsync(null!, content.Status, Status.Draft, null!, null!); + + Assert.True(result); + } + + [Fact] + public async Task Should_allow_if_transition_is_valid_for_content() { var content = new ContentEntity { Status = Status.Published }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs index b8cf8f8e9..fc8eb2f33 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs @@ -8,16 +8,19 @@ using System.Threading.Tasks; using FakeItEasy; using Orleans; +using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { public sealed class ContentCommandMiddlewareTests : HandlerTestBase { + private readonly IGrainFactory grainFactory = A.Fake(); private readonly IContentEnricher contentEnricher = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly DomainId contentId = DomainId.NewGuid(); @@ -30,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject protected override DomainId Id { - get { return contentId; } + get => contentId; } public ContentCommandMiddlewareTests() @@ -40,19 +43,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject A.CallTo(() => contextProvider.Context) .Returns(requestContext); - sut = new ContentCommandMiddleware(A.Fake(), contentEnricher, contextProvider); + sut = new ContentCommandMiddleware(grainFactory, contentEnricher, contextProvider); } [Fact] public async Task Should_not_invoke_enricher_for_other_result() { - var context = - CreateCommandContext( - new MyCommand()); - - context.Complete(12); - - await sut.HandleAsync(context); + await HandleAsync(new CreateContent(), 12); A.CallTo(() => contentEnricher.EnrichAsync(A._, A._, requestContext)) .MustNotHaveHappened(); @@ -64,12 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var result = new ContentEntity(); var context = - CreateCommandContext( - new MyCommand()); - - context.Complete(result); - - await sut.HandleAsync(context); + await HandleAsync(new CreateContent(), + result); Assert.Same(result, context.Result()); @@ -82,20 +75,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var result = A.Fake(); - var context = - CreateCommandContext( - new MyCommand()); - - context.Complete(result); - var enriched = new ContentEntity(); A.CallTo(() => contentEnricher.EnrichAsync(result, true, requestContext)) .Returns(enriched); - await sut.HandleAsync(context); + var context = + await HandleAsync(new CreateContent(), + result); Assert.Same(enriched, context.Result()); } + + private Task HandleAsync(ContentCommand command, object result) + { + command.ContentId = contentId; + + CreateCommand(command); + + var grain = A.Fake(); + + A.CallTo(() => grain.ExecuteAsync(A>._)) + .Returns(new CommandResult(command.AggregateId, 1, 0, result)); + + A.CallTo(() => grainFactory.GetGrain(command.AggregateId.ToString(), null)) + .Returns(grain); + + return HandleAsync(sut, command); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs index a8af30918..bdc4b8682 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs @@ -22,7 +22,6 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Validation; using Squidex.Log; using Xunit; @@ -70,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject protected override DomainId Id { - get { return DomainId.Combine(AppId, contentId); } + get => DomainId.Combine(AppId, contentId); } public ContentDomainObjectTests() @@ -126,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); await ExecuteDeleteAsync(); - await Assert.ThrowsAsync(ExecuteUpdateAsync); + await Assert.ThrowsAsync(ExecuteUpdateAsync); } [Fact] @@ -138,8 +137,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject result.ShouldBeEquivalent(sut.Snapshot); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); - Assert.Same(data, sut.Snapshot.CurrentVersion.Data); LastEvents .ShouldHaveSameEvents( @@ -153,30 +152,76 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } [Fact] - public async Task Create_should_create_events_and_update_status_when_publishing() + public async Task Create_should_not_change_status_when_set_to_initial() { - var command = new CreateContent { Data = data, Publish = true }; + var command = new CreateContent { Data = data, Status = Status.Draft }; var result = await PublishAsync(command); result.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Published, sut.Snapshot.Status); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.ExecuteAsync(A._, "", ScriptOptions())) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_change_status_when_set() + { + var command = new CreateContent { Data = data, Status = Status.Archived }; + + var result = await PublishAsync(command); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Archived, sut.Snapshot.Status); LastEvents .ShouldHaveSameEvents( CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) ); A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft), "", ScriptOptions())) .MustHaveHappened(); - A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Published), "", ScriptOptions())) + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "", ScriptOptions())) .MustHaveHappened(); } [Fact] - public async Task Create_should_throw_when_invalid_data_is_passed() + public async Task Create_should_recreate_deleted_content() + { + var command = new CreateContent { Data = data }; + + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await PublishAsync(command); + } + + [Fact] + public async Task Create_should_recreate_permanently_deleted_content() + { + var command = new CreateContent { Data = data }; + + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(true); + + await PublishAsync(command); + } + + [Fact] + public async Task Create_should_throw_exception_when_invalid_data_is_passed() { var command = new CreateContent { Data = invalidData }; @@ -184,7 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } [Fact] - public async Task Upsert_should_create_contnet_when_not_found() + public async Task Upsert_should_create_content_when_not_found() { var command = new UpsertContent { Data = data }; @@ -192,7 +237,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject result.ShouldBeEquivalent(sut.Snapshot); - Assert.Same(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.Status); LastEvents .ShouldHaveSameEvents( @@ -206,7 +252,54 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } [Fact] - public async Task Upsert_should_update_contnet_when_found() + public async Task Upsert_should_not_change_status_on_create_when_status_set_to_initial() + { + var command = new UpsertContent { Data = data }; + + var result = await PublishAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.ExecuteAsync(A._, "", ScriptOptions())) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Upsert_should_change_status_on_create_when_status_set() + { + var command = new UpsertContent { Data = data, Status = Status.Archived }; + + var result = await PublishAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Archived, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); + + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + } + + [Fact] + public async Task Upsert_should_update_content_when_found() { var command = new UpsertContent { Data = otherData }; @@ -227,6 +320,86 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject .MustHaveHappened(); } + [Fact] + public async Task Upsert_should_not_change_status_on_update_when_status_set_to_initial() + { + var command = new UpsertContent { Data = otherData, Status = Status.Draft }; + + await ExecuteCreateAsync(); + + var result = await PublishAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(otherData, data, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + } + + [Fact] + public async Task Upsert_should_change_status_on_update_when_status_set() + { + var command = new UpsertContent { Data = otherData, Status = Status.Archived }; + + await ExecuteCreateAsync(); + + var result = await PublishAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Archived, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); + + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(otherData, data, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(otherData, null, Status.Archived, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + } + + [Fact] + public async Task Upsert_should_recreate_deleted_content() + { + var command = new UpsertContent { Data = data }; + + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await PublishAsync(command); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + } + + [Fact] + public async Task Upsert_should_recreate_permanently_deleted_content() + { + var command = new UpsertContent { Data = data }; + + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(true); + + await PublishAsync(command); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + } + [Fact] public async Task Update_should_create_events_and_update_data() { @@ -291,7 +464,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } [Fact] - public async Task Update_should_throw_when_invalid_data_is_passed() + public async Task Update_should_throw_exception_when_invalid_data_is_passed() { var command = new UpdateContent { Data = invalidData }; @@ -366,7 +539,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject [Fact] public async Task ChangeStatus_should_create_events_and_update_status_when_published() { - var command = new ChangeContentStatus { Status = Status.Published }; + var command = new ChangeContentStatus { Status = Status.Archived }; await ExecuteCreateAsync(); @@ -374,14 +547,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject result.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Published, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(Status.Archived, sut.Snapshot.CurrentVersion.Status); LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) ); - A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Published, Status.Draft), "", ScriptOptions())) + A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "", ScriptOptions())) .MustHaveHappened(); } @@ -614,7 +787,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -627,6 +800,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject .MustHaveHappened(); } + [Fact] + public async Task Delete_should_not_create_events_if_permanent() + { + await ExecuteCreateAsync(); + + var command = new DeleteContent { Permanent = true }; + + var result = await PublishAsync(command); + + result.ShouldBeEquivalent(None.Value); + + Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); + Assert.Empty(LastEvents); + + A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Draft), "", ScriptOptions())) + .MustHaveHappened(); + } + [Fact] public async Task Delete_should_throw_exception_if_referenced_by_other_item() { @@ -684,7 +875,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var result = await PublishAsync(command); - result.ShouldBeEquivalent(new EntitySavedResult(3)); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.NewVersion); @@ -714,9 +905,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject return PublishAsync(new ChangeContentStatus { Status = status, DueTime = dueTime }); } - private Task ExecuteDeleteAsync() + private Task ExecuteDeleteAsync(bool permanent = false) { - return PublishAsync(CreateContentCommand(new DeleteContent())); + return PublishAsync(CreateContentCommand(new DeleteContent { Permanent = permanent })); } private Task ExecutePublishAsync() @@ -724,22 +915,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); } - private ScriptVars ScriptContext(ContentData? newData, ContentData? oldData, Status newStatus) + private static ScriptOptions ScriptOptions() { - return A.That.Matches(x => M(x, newData, oldData, newStatus, default)); + return A.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext); } - private ScriptVars ScriptContext(ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) + private ScriptVars ScriptContext(ContentData? newData, ContentData? oldData, Status newStatus) { - return A.That.Matches(x => M(x, newData, oldData, newStatus, oldStatus)); + return A.That.Matches(x => Matches(x, newData, oldData, newStatus, default)); } - private static ScriptOptions ScriptOptions() + private ScriptVars ScriptContext(ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) { - return A.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext); + return A.That.Matches(x => Matches(x, newData, oldData, newStatus, oldStatus)); } - private bool M(ScriptVars x, ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) + private bool Matches(ScriptVars x, ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) { return Equals(x.Data, newData) && @@ -749,25 +940,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject x.ContentId == contentId && x.User == User; } - protected T CreateContentEvent(T @event) where T : ContentEvent + private T CreateContentEvent(T @event) where T : ContentEvent { @event.ContentId = contentId; return CreateEvent(@event); } - protected T CreateContentCommand(T command) where T : ContentCommand + private T CreateContentCommand(T command) where T : ContentCommand { command.ContentId = contentId; return CreateCommand(command); } - private async Task PublishAsync(ContentCommand command) + private async Task PublishAsync(ContentCommand command) { var result = await sut.ExecuteAsync(CreateContentCommand(command)); - return result; + return result.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs similarity index 72% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs index 3c3b8af99..1f7adfdae 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs @@ -21,20 +21,21 @@ using Squidex.Shared; using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { - public class BulkUpdateCommandMiddlewareTests + public class ContentsBulkUpdateCommandMiddlewareTests { private readonly IContentQueryService contentQuery = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly ICommandBus commandBus = A.Dummy(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly BulkUpdateCommandMiddleware sut; + private readonly Instant time = Instant.FromDateTimeUtc(DateTime.UtcNow); + private readonly ContentsBulkUpdateCommandMiddleware sut; - public BulkUpdateCommandMiddlewareTests() + public ContentsBulkUpdateCommandMiddlewareTests() { - sut = new BulkUpdateCommandMiddleware(contentQuery, contextProvider); + sut = new ContentsBulkUpdateCommandMiddleware(contentQuery, contextProvider); } [Fact] @@ -64,11 +65,12 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateTestData(true); - var command = BulkCommand(BulkUpdateType.ChangeStatus); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == null && x.Exception is DomainObjectNotFoundException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainObjectNotFoundException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -89,11 +91,12 @@ namespace Squidex.Domain.Apps.Entities.Contents schemaId.Name, A.That.Matches(x => x.JsonQuery == query))) .Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id))); - var command = BulkCommand(BulkUpdateType.ChangeStatus, query); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, query); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == null && x.Exception is DomainException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -114,11 +117,12 @@ namespace Squidex.Domain.Apps.Entities.Contents schemaId.Name, A.That.Matches(x => x.JsonQuery == query))) .Returns(ResultList.CreateFrom(1, CreateContent(id))); - var command = BulkCommand(BulkUpdateType.Upsert, query: query, data: data); + var command = BulkCommand(BulkUpdateContentType.Upsert, query: query, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id))) @@ -145,14 +149,15 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContent(id1), CreateContent(id2))); - var command = BulkCommand(BulkUpdateType.Upsert, query: query, data: data); + var command = BulkCommand(BulkUpdateContentType.Upsert, query: query, data: data); command.Jobs![0].ExpectedCount = 2; var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id1 && x.Exception == null); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id2 && x.Exception == null); + Assert.Equal(2, result.Count); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id1 && x.Exception == null); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id2 && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id1))) @@ -170,14 +175,15 @@ namespace Squidex.Domain.Apps.Entities.Contents var (_, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Upsert, data: data); + var command = BulkCommand(BulkUpdateContentType.Upsert, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId != default && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId.ToString().Length == 36))) + A.That.Matches(x => x.Data == data && x.ContentId != default))) .MustHaveHappenedOnceExactly(); } @@ -188,14 +194,15 @@ namespace Squidex.Domain.Apps.Entities.Contents var (_, data, query) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Upsert, query: query, data: data); + var command = BulkCommand(BulkUpdateContentType.Upsert, query: query, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId != default && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId.ToString().Length == 36))) + A.That.Matches(x => x.Data == data && x.ContentId != default))) .MustHaveHappenedOnceExactly(); } @@ -206,11 +213,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Upsert, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Upsert, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId != default && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id))) @@ -224,11 +232,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(true); - var command = BulkCommand(BulkUpdateType.Upsert, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Upsert, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId != default && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id))) @@ -242,11 +251,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Create, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Create, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.Data == data))) @@ -260,11 +270,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Create, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Create, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception is DomainForbiddenException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -277,11 +288,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Update, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Update, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.Data == data))) @@ -295,11 +307,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Update, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Update, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception is DomainForbiddenException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -312,11 +325,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Patch, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Patch, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.Data == data))) @@ -330,11 +344,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Delete, id: id, data: data); + var command = BulkCommand(BulkUpdateContentType.Delete, id: id, data: data); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception is DomainForbiddenException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -343,15 +358,16 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_change_content_status() { - SetupContext(Permissions.AppContentsUpdateOwn); + SetupContext(Permissions.AppContentsChangeStatusOwn); var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.ChangeStatus, id: id); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.ContentId == id && x.DueTime == null))) .MustHaveHappened(); @@ -360,17 +376,16 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_change_content_status_with_due_time() { - SetupContext(Permissions.AppContentsUpdateOwn); - - var time = Instant.FromDateTimeUtc(DateTime.UtcNow); + SetupContext(Permissions.AppContentsChangeStatusOwn); var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.ChangeStatus, id: id, dueTime: time); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id, dueTime: time); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.ContentId == id && x.DueTime == time))) .MustHaveHappened(); @@ -383,11 +398,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.ChangeStatus, id: id); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception is DomainForbiddenException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -400,11 +416,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Validate, id: id); + var command = BulkCommand(BulkUpdateContentType.Validate, id: id); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id))) @@ -418,11 +435,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Validate, id: id); + var command = BulkCommand(BulkUpdateContentType.Validate, id: id); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception is DomainForbiddenException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -435,11 +453,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Delete, id: id); + var command = BulkCommand(BulkUpdateContentType.Delete, id: id); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception == null); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id))) @@ -453,11 +472,12 @@ namespace Squidex.Domain.Apps.Entities.Contents var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateType.Delete, id: id); + var command = BulkCommand(BulkUpdateContentType.Delete, id: id); var result = await PublishAsync(command); - Assert.Single(result, x => x.JobIndex == 0 && x.ContentId == id && x.Exception is DomainForbiddenException); + Assert.Single(result); + Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); @@ -472,7 +492,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return (context.PlainResult as BulkUpdateResult)!; } - private BulkUpdateContents BulkCommand(BulkUpdateType type, Query? query = null, + private BulkUpdateContents BulkCommand(BulkUpdateContentType type, Query? query = null, DomainId? id = null, ContentData? data = null, Instant? dueTime = null) { return new BulkUpdateContents diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs index 3c222c6da..c6dea3fb9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs @@ -8,7 +8,6 @@ using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; -using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; @@ -32,7 +31,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards private readonly ISchemaEntity schema; private readonly ISchemaEntity singleton; private readonly ClaimsPrincipal user = Mocks.FrontendUser(); - private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); private readonly RefToken actor = RefToken.User("123"); public GuardContentTests() @@ -45,61 +43,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards } [Fact] - public async Task CanCreate_should_throw_exception_if_data_is_null() + public void CanCreate_should_throw_exception_if_data_is_null() { var command = new CreateContent(); - await ValidationAssert.ThrowsAsync(() => GuardContent.CanCreate(command, workflow, schema), + ValidationAssert.Throws(() => GuardContent.CanCreate(command, schema), new ValidationError("Data is required.", "Data")); } [Fact] - public async Task CanCreate_should_throw_exception_if_singleton() + public void CanCreate_should_throw_exception_if_singleton() { var command = new CreateContent { Data = new ContentData() }; - await Assert.ThrowsAsync(() => GuardContent.CanCreate(command, workflow, singleton)); + Assert.Throws(() => GuardContent.CanCreate(command, singleton)); } [Fact] - public async Task CanCreate_should_not_throw_exception_if_singleton_and_id_is_schema_id() + public void CanCreate_should_not_throw_exception_if_singleton_and_id_is_schema_id() { var command = new CreateContent { Data = new ContentData(), ContentId = schema.Id }; - await GuardContent.CanCreate(command, workflow, schema); + GuardContent.CanCreate(command, schema); } [Fact] - public async Task CanCreate_should_throw_exception_if_publishing_not_allowed() - { - SetupCanCreatePublish(false); - - var command = new CreateContent { Data = new ContentData(), Publish = true }; - - await Assert.ThrowsAsync(() => GuardContent.CanCreate(command, workflow, schema)); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_publishing_allowed() - { - SetupCanCreatePublish(true); - - var command = new CreateContent { Data = new ContentData(), Publish = true }; - - await Assert.ThrowsAsync(() => GuardContent.CanCreate(command, workflow, schema)); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_data_is_not_null() + public void CanCreate_should_not_throw_exception_if_data_is_not_null() { var command = new CreateContent { Data = new ContentData() }; - await GuardContent.CanCreate(command, workflow, schema); + GuardContent.CanCreate(command, schema); } [Fact] public async Task CanUpdate_should_throw_exception_if_data_is_null() { + SetupCanUpdate(true); + var content = CreateContent(Status.Draft); var command = CreateCommand(new UpdateContent()); @@ -121,52 +101,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards } [Fact] - public async Task CanUpdate_should_not_throw_exception_if_data_is_not_null() - { - SetupCanUpdate(true); - - var content = CreateContent(Status.Draft); - - var command = CreateCommand(new UpdateContent { Data = new ContentData() }); - - await GuardContent.CanUpdate(command, content, workflow); - } - - [Fact] - public async Task CanPatch_should_throw_exception_if_data_is_null() - { - SetupCanUpdate(true); - - var content = CreateContent(Status.Draft); - - var command = CreateCommand(new PatchContent()); - - await ValidationAssert.ThrowsAsync(() => GuardContent.CanPatch(command, content, workflow), - new ValidationError("Data is required.", "Data")); - } - - [Fact] - public async Task CanPatch_should_throw_exception_if_workflow_blocks_it() + public async Task CanUpdate_should_throw_exception_if_workflow_blocks_it_but_check_is_disabled() { SetupCanUpdate(false); var content = CreateContent(Status.Draft); - var command = CreateCommand(new PatchContent { Data = new ContentData() }); + var command = CreateCommand(new UpdateContent { Data = new ContentData(), DoNotValidateWorkflow = true }); - await Assert.ThrowsAsync(() => GuardContent.CanPatch(command, content, workflow)); + await GuardContent.CanUpdate(command, content, workflow); } [Fact] - public async Task CanPatch_should_not_throw_exception_if_data_is_not_null() + public async Task CanUpdate_should_not_throw_exception_if_data_is_not_null() { SetupCanUpdate(true); var content = CreateContent(Status.Draft); - var command = CreateCommand(new PatchContent { Data = new ContentData() }); + var command = CreateCommand(new UpdateContent { Data = new ContentData() }); - await GuardContent.CanPatch(command, content, workflow); + await GuardContent.CanUpdate(command, content, workflow); } [Fact] @@ -180,31 +135,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards } [Fact] - public async Task CanChangeStatus_should_throw_exception_if_due_date_in_past() + public async Task CanChangeStatus_should_throw_exception_if_status_flow_not_valid() { var content = CreateContent(Status.Draft); - var command = CreateCommand(new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast }); + var command = CreateCommand(new ChangeContentStatus { Status = Status.Published }); A.CallTo(() => workflow.CanMoveToAsync(content, content.Status, command.Status, user)) - .Returns(true); + .Returns(false); await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(command, content, workflow, contentRepository, schema), - new ValidationError("Due time must be in the future.", "DueTime")); + new ValidationError("Cannot change status from Draft to Published.", "Status")); } [Fact] - public async Task CanChangeStatus_should_throw_exception_if_status_flow_not_valid() + public async Task CanChangeStatus_should_throw_exception_if_status_valid() { var content = CreateContent(Status.Draft); - var command = CreateCommand(new ChangeContentStatus { Status = Status.Published }); + var command = CreateCommand(new ChangeContentStatus { Status = new Status("Invalid"), DoNotValidateWorkflow = true }); - A.CallTo(() => workflow.CanMoveToAsync(content, content.Status, command.Status, user)) - .Returns(false); + A.CallTo(() => workflow.GetInfoAsync(content, command.Status)) + .Returns(Task.FromResult(null)); await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(command, content, workflow, contentRepository, schema), - new ValidationError("Cannot change status from Draft to Published.", "Status")); + new ValidationError("Status is not defined in the workflow.", "Status")); } [Fact] @@ -220,6 +175,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards await Assert.ThrowsAsync(() => GuardContent.CanChangeStatus(command, content, workflow, contentRepository, schema)); } + [Fact] + public async Task CanChangeStatus_should_not_throw_exception_if_status_flow_not_valid_but_check_disabled() + { + var content = CreateContent(Status.Draft); + + var command = CreateCommand(new ChangeContentStatus { Status = Status.Published, DoNotValidateWorkflow = true }); + + A.CallTo(() => workflow.CanMoveToAsync(content, content.Status, command.Status, user)) + .Returns(false); + + await GuardContent.CanChangeStatus(command, content, workflow, contentRepository, schema); + } + [Fact] public async Task CanChangeStatus_should_not_throw_exception_if_singleton_is_published() { @@ -370,12 +338,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards .Returns(canUpdate); } - private void SetupCanCreatePublish(bool canCreate) - { - A.CallTo(() => workflow.CanPublishOnCreateAsync(schema, A._, user)) - .Returns(canCreate); - } - private T CreateCommand(T command) where T : ContentCommand { if (command.Actor == null) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index d0fe7da8e..d8860c934 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -96,6 +96,26 @@ namespace Squidex.Domain.Apps.Entities.Contents sut = new DynamicContentWorkflow(new JintScriptEngine(memoryCache), appProvider); } + [Fact] + public async Task Should_return_info_for_valid_status() + { + var content = CreateContent(Status.Draft, 2); + + var info = await sut.GetInfoAsync(content, Status.Draft); + + Assert.Equal(new StatusInfo(Status.Draft, StatusColors.Draft), info); + } + + [Fact] + public async Task Should_return_info_as_null_for_invalid_status() + { + var content = CreateContent(Status.Draft, 2); + + var info = await sut.GetInfoAsync(content, new Status("Invalid")); + + Assert.Null(info); + } + [Fact] public async Task Should_return_draft_as_initial_status() { @@ -135,7 +155,17 @@ namespace Squidex.Domain.Apps.Entities.Contents } [Fact] - public async Task Should_check_is_valid_next() + public async Task Should_allow_if_transition_is_valid() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanMoveToAsync(Mocks.Schema(appId, schemaId), content.Status, Status.Published, content.Data, Mocks.FrontendUser("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_allow_if_transition_is_valid_for_content() { var content = CreateContent(Status.Draft, 2); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index 7593f1027..a3c7db0cb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -12,7 +12,7 @@ using GraphQL; using GraphQL.NewtonsoftJson; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using NodaTime; +using NodaTime.Text; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; @@ -80,12 +80,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_creating_content() { - var query = @" + var query = CreateQuery(@" mutation { createMySchemaContent(data: , publish: true) { } - }".Replace("", GetDataString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -103,9 +103,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schemaId) && x.ExpectedVersion == EtagVersion.Any && - x.Publish && + x.SchemaId.Equals(schemaId) && + x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -113,12 +113,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_creating_content_with_custom_id() { - var query = @" + var query = CreateQuery(@" mutation { - createMySchemaContent(data: , id: ""123"", publish: true) { + createMySchemaContent(data: , id: '123', publish: true) { } - }".Replace("", GetDataString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -136,10 +136,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schemaId) && x.ExpectedVersion == EtagVersion.Any && x.ContentId == DomainId.Create("123") && - x.Publish && + x.SchemaId.Equals(schemaId) && + x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -147,12 +147,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_creating_content_with_variable() { - var query = @" + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { createMySchemaContent(data: $data, publish: true) { } - }".Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -170,9 +170,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schemaId) && x.ExpectedVersion == EtagVersion.Any && - x.Publish && + x.SchemaId.Equals(schemaId) && + x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -180,12 +180,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_error_when_user_has_no_permission_to_update() { - var query = @" + var query = CreateQuery(@" mutation { - updateMySchemaContent(id: """", data: { myNumber: { iv: 42 } }) { + updateMySchemaContent(id: '', data: { myNumber: { iv: 42 } }) { id } - }".Replace("", contentId.ToString()); + }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); @@ -221,12 +221,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_updating_content() { - var query = @" + var query = CreateQuery(@" mutation { - updateMySchemaContent(id: """", data: , expectedVersion: 10) { + updateMySchemaContent(id: '', data: , expectedVersion: 10) { } - }".Replace("", contentId.ToString()).Replace("", GetDataString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -246,6 +246,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && + x.SchemaId.Equals(schemaId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -253,12 +254,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_updating_content_with_variable() { - var query = @" + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { - updateMySchemaContent(id: """", data: $data, expectedVersion: 10) { + updateMySchemaContent(id: '', data: $data, expectedVersion: 10) { } - }".Replace("", contentId.ToString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -278,6 +279,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && + x.SchemaId.Equals(schemaId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -285,12 +287,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_error_when_user_has_no_permission_to_upsert() { - var query = @" + var query = CreateQuery(@" mutation { - upsertMySchemaContent(id: """", data: { myNumber: { iv: 42 } }) { + upsertMySchemaContent(id: '', data: { myNumber: { iv: 42 } }) { id } - }".Replace("", contentId.ToString()); + }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); @@ -326,12 +328,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_upserting_content() { - var query = @" + var query = CreateQuery(@" mutation { - upsertMySchemaContent(id: """", data: , publish: true, expectedVersion: 10) { + upsertMySchemaContent(id: '', data: , publish: true, expectedVersion: 10) { } - }".Replace("", contentId.ToString()).Replace("", GetDataString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -351,7 +353,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && - x.Publish && + x.SchemaId.Equals(schemaId) && + x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -359,12 +362,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_upserting_content_with_variable() { - var query = @" + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { - upsertMySchemaContent(id: """", data: $data, publish: true, expectedVersion: 10) { + upsertMySchemaContent(id: '', data: $data, publish: true, expectedVersion: 10) { } - }".Replace("", contentId.ToString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -384,7 +387,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && - x.Publish && + x.SchemaId.Equals(schemaId) && + x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -392,12 +396,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_error_when_user_has_no_permission_to_patch() { - var query = @" + var query = CreateQuery(@" mutation { - patchMySchemaContent(id: """", data: { myNumber: { iv: 42 } }) { + patchMySchemaContent(id: '', data: { myNumber: { iv: 42 } }) { id } - }".Replace("", contentId.ToString()); + }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); @@ -433,12 +437,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_patching_content() { - var query = @" + var query = CreateQuery(@" mutation { - patchMySchemaContent(id: """", data: , expectedVersion: 10) { + patchMySchemaContent(id: '', data: , expectedVersion: 10) { } - }".Replace("", contentId.ToString()).Replace("", GetDataString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -458,6 +462,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && + x.SchemaId.Equals(schemaId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -465,12 +470,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_patching_content_with_variable() { - var query = @" + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { - patchMySchemaContent(id: """", data: $data, expectedVersion: 10) { + patchMySchemaContent(id: '', data: $data, expectedVersion: 10) { } - }".Replace("", contentId.ToString()).Replace("", TestContent.AllFields); + }"); commandContext.Complete(content); @@ -490,6 +495,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && + x.SchemaId.Equals(schemaId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } @@ -497,12 +503,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_error_when_user_has_no_permission_to_change_status() { - var query = @" + var query = CreateQuery(@" mutation { - changeMySchemaContent(id: """", status: ""Published"") { + changeMySchemaContent(id: '', status: 'Published') { id } - }".Replace("", contentId.ToString()); + }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); @@ -538,14 +544,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [Fact] public async Task Should_return_single_content_when_changing_status() { - var dueTime = SystemClock.Instance.GetCurrentInstant().WithoutMs(); + var dueTime = InstantPattern.General.Parse("2021-12-12T11:10:09Z").Value; - var query = @" + var query = CreateQuery(@" mutation { - changeMySchemaContent(id: """", status: ""Published"", dueTime: ""