diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 000314ccb..755e30a1d 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -61,17 +61,17 @@ namespace Squidex.Domain.Apps.Entities.Contents case CreateContent createContent: return CreateReturnAsync(createContent, async c => { - GuardContent.CanCreate(c); + var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, () => "Failed to create content."); - var operationContext = await CreateContext(c.AppId.Id, c.SchemaId.Id, () => "Failed to create content."); + GuardContent.CanCreate(ctx.Schema, c); - await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create", c, c.Data, null); - await operationContext.EnrichAsync(c.Data); - await operationContext.ValidateAsync(c.Data); + await ctx.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create", c, c.Data, null); + await ctx.EnrichAsync(c.Data); + await ctx.ValidateAsync(c.Data); if (c.Publish) { - await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published", c, c.Data, null); + await ctx.ExecuteScriptAsync(x => x.ScriptChange, "Published", c, c.Data, null); } Create(c); @@ -100,7 +100,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { - GuardContent.CanChangeContentStatus(Snapshot.IsPending, Snapshot.Status, c); + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to change content."); + + GuardContent.CanChangeContentStatus(ctx.Schema, Snapshot.IsPending, Snapshot.Status, c); if (c.DueTime.HasValue) { @@ -114,8 +116,6 @@ namespace Squidex.Domain.Apps.Entities.Contents } else { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to change content."); - await ctx.ExecuteScriptAsync(x => x.ScriptChange, c.Status, c, Snapshot.Data); ChangeStatus(c); @@ -138,11 +138,11 @@ namespace Squidex.Domain.Apps.Entities.Contents case DeleteContent deleteContent: return UpdateAsync(deleteContent, async c => { - GuardContent.CanDelete(c); + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to delete content."); - var operationContext = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to delete content."); + GuardContent.CanDelete(ctx.Schema, c); - await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete", c, Snapshot.Data); + await ctx.ExecuteScriptAsync(x => x.ScriptDelete, "Delete", c, Snapshot.Data); Delete(c); }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index 5b1bc9474..97e729321 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -31,6 +31,11 @@ namespace Squidex.Domain.Apps.Entities.Contents private IAppEntity appEntity; private Func message; + public ISchemaEntity Schema + { + get { return schemaEntity; } + } + public static async Task CreateAsync( Guid appId, Guid schemaId, diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index f2602ccbc..5bcb4c004 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -63,7 +63,6 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task FindContentAsync(QueryContext context, Guid id, long version = -1) { Guard.NotNull(context, nameof(context)); - Guard.NotEmpty(id, nameof(id)); var schema = await GetSchemaAsync(context); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 8f5e4d1b8..8c02f6d88 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -5,16 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Guards { public static class GuardContent { - public static void CanCreate(CreateContent command) + public static void CanCreate(ISchemaEntity schema, CreateContent command) { Guard.NotNull(command, nameof(command)); @@ -22,6 +24,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { ValidateData(command, e); }); + + if (schema.IsSingleton && command.ContentId != Guid.Empty) + { + throw new DomainException("Singleton content cannot be created."); + } } public static void CanUpdate(UpdateContent command) @@ -54,10 +61,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards } } - public static void CanChangeContentStatus(bool isPending, Status status, ChangeContentStatus command) + public static void CanChangeContentStatus(ISchemaEntity schema, bool isPending, Status status, ChangeContentStatus command) { Guard.NotNull(command, nameof(command)); + if (schema.IsSingleton && command.Status != Status.Published) + { + throw new DomainException("Singleton content archived or unpublished."); + } + Validate.It(() => "Cannot change status.", e => { if (!StatusFlow.Exists(command.Status)) @@ -86,9 +98,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards }); } - public static void CanDelete(DeleteContent command) + public static void CanDelete(ISchemaEntity schema, DeleteContent command) { Guard.NotNull(command, nameof(command)); + + if (schema.IsSingleton) + { + throw new DomainException("Singleton content cannot be deleted."); + } } private static void ValidateData(ContentDataCommand command, AddValidation e) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs new file mode 100644 index 000000000..6dd070600 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class SingletonCommandMiddleware : ICommandMiddleware + { + public async Task HandleAsync(CommandContext context, Func next) + { + await next(); + + if (context.IsCompleted && + context.Command is CreateSchema createSchema && + createSchema.Singleton) + { + var schemaId = new NamedId(createSchema.SchemaId, createSchema.Name); + + var data = new NamedContentData(); + + var contentId = Guid.Empty; + var content = new CreateContent { Data = data, ContentId = contentId, SchemaId = schemaId, Publish = true }; + + SimpleMapper.Map(context.Command, content); + + await context.CommandBus.PublishAsync(content); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs index 923499e79..9c39fa5a9 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -22,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public SchemaProperties Properties { get; set; } + public bool Singleton { get; set; } + public bool Publish { get; set; } public CreateSchema() diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs index 335a91e92..98913f9e3 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas string Category { get; } + bool IsSingleton { get; } + bool IsPublished { get; } bool IsDeleted { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 4a787d912..01bd7a7d1 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -37,6 +37,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State [JsonProperty] public bool IsDeleted { get; set; } + [JsonProperty] + public bool IsSingleton { get; set; } + [JsonProperty] public string ScriptQuery { get; set; } @@ -65,6 +68,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State { Name = @event.Name; + IsSingleton = @event.Singleton; + var schema = new Schema(@event.Name); if (@event.Properties != null) diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs b/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs index 2e7c6ec2b..6f4f24295 100644 --- a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs @@ -20,6 +20,8 @@ namespace Squidex.Domain.Apps.Events.Schemas public SchemaProperties Properties { get; set; } + public bool Singleton { get; set; } + public bool Publish { get; set; } } } diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index 461c443fb..89e162a54 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -42,7 +42,7 @@ namespace Squidex.Infrastructure.States public IPersistence WithEventSourcing(Type owner, TKey key, Func, Task> applyEvent) { - Guard.NotDefault(key, nameof(key)); + Guard.NotNull(key, nameof(key)); var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); @@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.States private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) { - Guard.NotDefault(key, nameof(key)); + Guard.NotNull(key, nameof(key)); var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs index b8109375f..922647abc 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs @@ -32,6 +32,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public List Fields { get; set; } + /// + /// Set to true to allow a single content item only. + /// + public bool Singleton { get; set; } + /// /// Set it to true to autopublish the schema. /// diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs index 850c6f938..c9c3c5909 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs @@ -36,6 +36,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public string Category { get; set; } + /// + /// Indicates if the schema is a singleton. + /// + public bool IsSingleton { get; set; } + /// /// Indicates if the schema is published. /// diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 0c0869ede..370d6a91d 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -39,6 +39,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models [Required] public SchemaPropertiesDto Properties { get; set; } + /// + /// Indicates if the schema is a singleton. + /// + public bool IsSingleton { get; set; } + /// /// Indicates if the schema is published. /// diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index ab8132503..140a66cc6 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -128,6 +128,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index d6442b2a9..cdf90089e 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -3,7 +3,7 @@
- + @@ -32,7 +32,8 @@ + + + + +