Browse Source

Simplified content status system.

pull/111/head
Sebastian Stehle 9 years ago
parent
commit
7c7a066e15
  1. 9
      src/Squidex.Domain.Apps.Core/Contents/Status.cs
  2. 33
      src/Squidex.Domain.Apps.Core/Contents/StatusFlow.cs
  3. 2
      src/Squidex.Domain.Apps.Core/Webhooks/WebhookSchema.cs
  4. 19
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs
  5. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentArchived.cs
  6. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentPublished.cs
  7. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentRestored.cs
  8. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentUnpublished.cs
  9. 13
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs
  10. 9
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs
  11. 25
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  12. 14
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs
  13. 5
      src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs
  14. 33
      src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs
  15. 5
      src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs
  16. 5
      src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs
  17. 12
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs
  18. 9
      src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs
  19. 14
      src/Squidex.Domain.Apps.Write/Contents/Commands/PublishContent.cs
  20. 14
      src/Squidex.Domain.Apps.Write/Contents/Commands/UnpublishContent.cs
  21. 47
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs
  22. 79
      src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs
  23. 5
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs
  24. 8
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  25. 11
      src/Squidex/Controllers/ContentApi/Models/ContentDto.cs
  26. 2
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  27. 12
      src/Squidex/app/features/content/shared/content-item.component.html
  28. 15
      src/Squidex/app/features/webhooks/pages/webhook.component.html
  29. 17
      src/Squidex/app/features/webhooks/pages/webhook.component.ts
  30. 44
      src/Squidex/app/shared/services/contents.service.spec.ts
  31. 51
      src/Squidex/app/shared/services/contents.service.ts
  32. 14
      src/Squidex/app/shared/services/webhooks.service.spec.ts
  33. 6
      src/Squidex/app/shared/services/webhooks.service.ts
  34. 34
      tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs
  35. 44
      tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs
  36. 13
      tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs
  37. 60
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs
  38. 139
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs
  39. 2
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs

9
src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs → src/Squidex.Domain.Apps.Core/Contents/Status.cs

@ -1,14 +1,17 @@
// ==========================================================================
// RestoreContent.cs
// Status.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class RestoreContent : ContentCommand
public enum Status
{
Draft,
Archived,
Published
}
}

33
src/Squidex.Domain.Apps.Core/Contents/StatusFlow.cs

@ -0,0 +1,33 @@
// ==========================================================================
// StatusFlow.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusFlow
{
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[Status.Draft] = new[] { Status.Published, Status.Archived },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
};
public static bool Exists(Status status)
{
return Flow.ContainsKey(status);
}
public static bool CanChange(Status status, Status toStatus)
{
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus);
}
}
}

2
src/Squidex.Domain.Apps.Core/Webhooks/WebhookSchema.cs

@ -21,7 +21,5 @@ namespace Squidex.Domain.Apps.Core.Webhooks
public bool SendDelete { get; set; }
public bool SendPublish { get; set; }
public bool SendUnpublish { get; set; }
}
}

19
src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs

@ -0,0 +1,19 @@
// ==========================================================================
// ContentStatusChanged.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentStatusChanged))]
public sealed class ContentStatusChanged : ContentEvent
{
public Status Status { get; set; }
}
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentArchived.cs

@ -6,11 +6,13 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentArchived))]
[Obsolete]
public sealed class ContentArchived : ContentEvent
{
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentPublished.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentPublished.cs

@ -6,11 +6,13 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentPublished))]
[Obsolete]
public sealed class ContentPublished : ContentEvent
{
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentRestored.cs

@ -6,11 +6,13 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentRestored))]
[Obsolete]
public sealed class ContentRestored : ContentEvent
{
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentUnpublished.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentUnpublished.cs

@ -6,11 +6,13 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentUnpublished))]
[Obsolete]
public sealed class ContentUnpublished : ContentEvent
{
}

13
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs

@ -35,6 +35,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
[BsonRequired]
[BsonElement("st")]
[BsonRepresentation(BsonType.String)]
public Status Status { get; set; }
[BsonRequired]
[BsonElement("ct")]
public Instant Created { get; set; }
@ -43,14 +48,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
[BsonElement("mt")]
public Instant LastModified { get; set; }
[BsonRequired]
[BsonElement("pu")]
public bool IsPublished { get; set; }
[BsonRequired]
[BsonElement("dl")]
public bool IsArchived { get; set; }
[BsonRequired]
[BsonElement("dt")]
public string DataText { get; set; }

9
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs

@ -13,6 +13,7 @@ using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.Repositories;
@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
this.database = database;
}
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery)
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery)
{
var collection = GetCollection(app.Id);
@ -80,7 +81,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
cursor =
collection
.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived)
.Find(odataQuery, ids, schema.Id, schema.SchemaDef, status)
.Take(odataQuery)
.Skip(odataQuery)
.Sort(odataQuery, schema.SchemaDef);
@ -104,14 +105,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
return entities;
}
public Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery)
public Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery)
{
var collection = GetCollection(app.Id);
IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try
{
cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived);
cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, status);
}
catch (NotSupportedException)
{

25
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -9,6 +9,7 @@
using System;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
@ -17,6 +18,8 @@ using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
#pragma warning disable CS0612 // Type or member is obsolete
namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
public partial class MongoContentRepository
@ -61,8 +64,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsPublished));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsArchived));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Status));
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText));
});
}
@ -99,7 +101,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
return collection.UpdateAsync(@event, headers, x =>
{
x.IsPublished = true;
x.Status = Status.Published;
});
});
}
@ -110,7 +112,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
return collection.UpdateAsync(@event, headers, x =>
{
x.IsPublished = false;
x.Status = Status.Draft;
});
});
}
@ -121,7 +123,18 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
return collection.UpdateAsync(@event, headers, x =>
{
x.IsArchived = true;
x.Status = Status.Archived;
});
});
}
protected Task On(ContentStatusChanged @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, collection =>
{
return collection.UpdateAsync(@event, headers, x =>
{
x.Status = @event.Status;
});
});
}
@ -132,7 +145,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
return collection.UpdateAsync(@event, headers, x =>
{
x.IsArchived = false;
x.Status = Status.Draft;
});
});
}

14
src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs

@ -11,6 +11,7 @@ using System.Collections.Generic;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
@ -56,26 +57,21 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
return cursor;
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished, bool archived)
public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, Status[] status)
{
var filter = BuildQuery(query, ids, schemaId, schema, nonPublished, archived);
var filter = BuildQuery(query, ids, schemaId, schema, status);
return cursor.Find(filter);
}
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished, bool archived)
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, Status[] status)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.SchemaId, schemaId),
Filter.Eq(x => x.IsArchived, archived)
Filter.In(x => x.Status, status)
};
if (!nonPublished)
{
filters.Add(Filter.Eq(x => x.IsPublished, true));
}
if (ids != null && ids.Count > 0)
{
filters.Add(Filter.In(x => x.Id, ids));

5
src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs

@ -12,6 +12,8 @@ using Squidex.Domain.Apps.Read.History;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
#pragma warning disable CS0612 // Type or member is obsolete
namespace Squidex.Domain.Apps.Read.Contents
{
public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase
@ -36,6 +38,9 @@ namespace Squidex.Domain.Apps.Read.Contents
AddEventMessage<ContentUnpublished>(
"unpublished content item.");
AddEventMessage<ContentStatusChanged>(
"change status of content item to {[Status]}.");
}
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)

33
src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs

@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Read.Contents
var content = await contentRepository.FindContentAsync(app, schema, id);
if (content == null || (!content.IsPublished && !isFrontendClient))
if (content == null || (content.Status != Status.Published && !isFrontendClient))
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity));
}
@ -83,10 +83,27 @@ namespace Squidex.Domain.Apps.Read.Contents
var parsedQuery = ParseQuery(app, query, schema);
var isFrontendClient = user.IsInClient("squidex-frontend");
var status = new List<Status>();
var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, archived, ids, parsedQuery);
var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, archived, ids, parsedQuery);
if (user.IsInClient("squidex-frontend"))
{
if (archived)
{
status.Add(Status.Archived);
}
else
{
status.Add(Status.Draft);
status.Add(Status.Published);
}
}
else
{
status.Add(Status.Published);
}
var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), ids, parsedQuery);
var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), ids, parsedQuery);
await Task.WhenAll(taskForItems, taskForCount);
@ -155,14 +172,18 @@ namespace Squidex.Domain.Apps.Read.Contents
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public long Version { get; set; }
public bool IsArchived { get; set; }
public bool IsPublished { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public NamedContentData Data { get; set; }
public Status Status { get; set; }
}
}
}

5
src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs

@ -5,6 +5,7 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
@ -12,9 +13,7 @@ namespace Squidex.Domain.Apps.Read.Contents
{
public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion
{
bool IsPublished { get; }
bool IsArchived { get; }
Status Status { get; }
NamedContentData Data { get; }
}

5
src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Schemas;
@ -17,11 +18,11 @@ namespace Squidex.Domain.Apps.Read.Contents.Repositories
{
public interface IContentRepository
{
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds);
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id);
}

12
src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Webhooks;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
@ -115,12 +116,11 @@ namespace Squidex.Domain.Apps.Read.Webhooks
private static bool Matchs(WebhookSchema webhookSchema, SchemaEvent @event)
{
return
(@event.SchemaId.Id == webhookSchema.SchemaId) &&
(@event is ContentCreated && webhookSchema.SendCreate ||
@event is ContentUpdated && webhookSchema.SendUpdate ||
@event is ContentDeleted && webhookSchema.SendDelete ||
@event is ContentPublished && webhookSchema.SendPublish ||
@event is ContentUnpublished && webhookSchema.SendUnpublish);
(@event.SchemaId.Id == webhookSchema.SchemaId) &&
(webhookSchema.SendCreate && @event is ContentCreated ||
webhookSchema.SendUpdate && @event is ContentUpdated ||
webhookSchema.SendDelete && @event is ContentDeleted ||
webhookSchema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published);
}
private string CreatePayload(Envelope<IEvent> @event, string eventType)

9
src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs → src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs

@ -1,14 +1,17 @@
// ==========================================================================
// ArchiveContent.cs
// ChangeContentStatus.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
// =========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public sealed class ArchiveContent : ContentCommand
public sealed class ChangeContentStatus : ContentCommand
{
public Status Status { get; set; }
}
}

14
src/Squidex.Domain.Apps.Write/Contents/Commands/PublishContent.cs

@ -1,14 +0,0 @@
// ==========================================================================
// PublishContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public sealed class PublishContent : ContentCommand
{
}
}

14
src/Squidex.Domain.Apps.Write/Contents/Commands/UnpublishContent.cs

@ -1,14 +0,0 @@
// ==========================================================================
// UnpublishContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public sealed class UnpublishContent : ContentCommand
{
}
}

47
src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs

@ -111,55 +111,16 @@ namespace Squidex.Domain.Apps.Write.Contents
});
}
protected Task On(PublishContent command, CommandContext context)
protected Task On(ChangeContentStatus command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Publish");
var scriptContext = CreateScriptContext(content, command, command.Status.ToString());
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "publish content");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "change content status");
content.Publish(command);
});
}
protected Task On(UnpublishContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Unpublish");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "unpublish content");
content.Unpublish(command);
});
}
protected Task On(ArchiveContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Archive");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "archive content");
content.Archive(command);
});
}
protected Task On(RestoreContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Restore");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "restore content");
content.Restore(command);
content.ChangeStatus(command);
});
}

79
src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs

@ -22,8 +22,7 @@ namespace Squidex.Domain.Apps.Write.Contents
{
private bool isDeleted;
private bool isCreated;
private bool isPublished;
private bool isArchived;
private Status status;
private NamedContentData data;
public bool IsDeleted
@ -31,14 +30,9 @@ namespace Squidex.Domain.Apps.Write.Contents
get { return isDeleted; }
}
public bool IsArchived
public Status Status
{
get { return isArchived; }
}
public bool IsPublished
{
get { return isPublished; }
get { return status; }
}
public NamedContentData Data
@ -63,24 +57,9 @@ namespace Squidex.Domain.Apps.Write.Contents
data = @event.Data;
}
protected void On(ContentPublished @event)
{
isPublished = true;
}
protected void On(ContentUnpublished @event)
{
isPublished = false;
}
protected void On(ContentArchived @event)
{
isArchived = true;
}
protected void On(ContentRestored @event)
protected void On(ContentStatusChanged @event)
{
isArchived = false;
status = @event.Status;
}
protected void On(ContentDeleted @event)
@ -98,7 +77,7 @@ namespace Squidex.Domain.Apps.Write.Contents
if (command.Publish)
{
RaiseEvent(SimpleMapper.Map(command, new ContentPublished()));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
}
return this;
@ -115,46 +94,14 @@ namespace Squidex.Domain.Apps.Write.Contents
return this;
}
public ContentDomainObject Restore(RestoreContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentRestored()));
return this;
}
public ContentDomainObject Archive(ArchiveContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentArchived()));
return this;
}
public ContentDomainObject Publish(PublishContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentPublished()));
return this;
}
public ContentDomainObject Unpublish(UnpublishContent command)
public ContentDomainObject ChangeStatus(ChangeContentStatus command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
VerifyCanChangeStatus(command.Status);
RaiseEvent(SimpleMapper.Map(command, new ContentUnpublished()));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged()));
return this;
}
@ -189,6 +136,14 @@ namespace Squidex.Domain.Apps.Write.Contents
return this;
}
private void VerifyCanChangeStatus(Status newStatus)
{
if (!StatusFlow.Exists(newStatus) && !StatusFlow.CanChange(status, newStatus))
{
throw new DomainException($"Content cannot be changed from status {status} to {newStatus}.");
}
}
private void VerifyNotCreated()
{
if (isCreated)

5
src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs

@ -36,10 +36,5 @@ namespace Squidex.Controllers.Api.Webhooks.Models
/// True, when to send a message for published events.
/// </summary>
public bool SendPublish { get; set; }
/// <summary>
/// True, when to send a message for unpublished events.
/// </summary>
public bool SendUnpublish { get; set; }
}
}

8
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -206,7 +206,7 @@ namespace Squidex.Controllers.ContentApi
{
await contentQuery.FindSchemaAsync(App, name);
var command = new PublishContent { ContentId = id, User = User };
var command = new ChangeContentStatus { Status = Status.Published, ContentId = id, User = User };
await CommandBus.PublishAsync(command);
@ -221,7 +221,7 @@ namespace Squidex.Controllers.ContentApi
{
await contentQuery.FindSchemaAsync(App, name);
var command = new UnpublishContent { ContentId = id, User = User };
var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id, User = User };
await CommandBus.PublishAsync(command);
@ -236,7 +236,7 @@ namespace Squidex.Controllers.ContentApi
{
await contentQuery.FindSchemaAsync(App, name);
var command = new ArchiveContent { ContentId = id, User = User };
var command = new ChangeContentStatus { Status = Status.Archived, ContentId = id, User = User };
await CommandBus.PublishAsync(command);
@ -251,7 +251,7 @@ namespace Squidex.Controllers.ContentApi
{
await contentQuery.FindSchemaAsync(App, name);
var command = new RestoreContent { ContentId = id, User = User };
var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id, User = User };
await CommandBus.PublishAsync(command);

11
src/Squidex/Controllers/ContentApi/Models/ContentDto.cs

@ -52,14 +52,9 @@ namespace Squidex.Controllers.ContentApi.Models
public Instant LastModified { get; set; }
/// <summary>
/// Indicates if the content item is published.
/// Gets the status of the content.
/// </summary>
public bool? IsPublished { get; set; }
/// <summary>
/// Indicates if the content item is archived.
/// </summary>
public bool IsArchived { get; set; }
public Status Status { get; set; }
/// <summary>
/// The version of the content.
@ -79,7 +74,7 @@ namespace Squidex.Controllers.ContentApi.Models
CreatedBy = command.Actor,
LastModified = now,
LastModifiedBy = command.Actor,
IsPublished = command.Publish
Status = command.Publish ? Status.Published : Status.Draft
};
return response;

2
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -233,7 +233,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
}
}
if (this.content.isArchived) {
if (this.content.status === 'Archived') {
this.contentForm.disable();
}
}

12
src/Squidex/app/features/content/shared/content-item.component.html

@ -4,7 +4,7 @@
</span>
</td>
<td>
<span class="item-published" [class.unpublished]="!content.isPublished"></span>
<span class="item-published" [class.unpublished]="content.status !== 'Published'"></span>
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
@ -17,19 +17,19 @@
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="!content.isPublished && !content.isArchived">
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Draft'">
Publish
</a>
<a class="dropdown-item" (click)="unpublishing.emit(); $event.stopPropagation()" *ngIf="content.isPublished && !content.isArchived">
<a class="dropdown-item" (click)="unpublishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Published'">
Unpublish
</a>
<a class="dropdown-item" (click)="archiving.emit(); $event.stopPropagation()" *ngIf="!content.isArchived">
<a class="dropdown-item" (click)="archiving.emit(); $event.stopPropagation()" *ngIf="content.status !== 'Archived'">
Archive
</a>
<a class="dropdown-item" (click)="restoring.emit(); $event.stopPropagation()" *ngIf="content.isArchived">
<a class="dropdown-item" (click)="restoring.emit(); $event.stopPropagation()" *ngIf="content.status === 'Archived'">
Restore
</a>
<a class="dropdown-item dropdown-item-delete" *ngIf="content.isArchived"
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="deleting.emit()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">

15
src/Squidex/app/features/webhooks/pages/webhook.component.html

@ -55,7 +55,6 @@
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
</colgroup>
<tr>
@ -66,19 +65,16 @@
All
</th>
<th class="text-center" title="Created">
Cr
C
</th>
<th class="text-center" title="Updated">
Up
U
</th>
<th class="text-center" title="Deleted">
Dl
D
</th>
<th class="text-center" title="Published">
Pu
</th>
<th class="text-center" title="Unpublished">
Up
P
</th>
<th></th>
</tr>
@ -102,9 +98,6 @@
<td class="text-center" title="Published">
<input type="checkbox" [ngModel]="schema.sendPublish" (ngModelChange)="toggle(schema, 'sendPublish')" />
</td>
<td class="text-center" title="Unpublished">
<input type="checkbox" [ngModel]="schema.sendUnpublish" (ngModelChange)="toggle(schema, 'sendUnpublish')" />
</td>
<td class="text-center">
<button type="button" class="btn btn-link btn-secondary"
(sqxConfirmClick)="removeSchema(schema)"

17
src/Squidex/app/features/webhooks/pages/webhook.component.ts

@ -22,8 +22,7 @@ export interface WebhookSchemaForm {
sendCreate: boolean;
sendUpdate: boolean;
sendDelete: boolean;
sendPublish: boolean;
sendUnpublish: boolean;
sendPublish: boolean
}
@Component({
@ -85,8 +84,7 @@ export class WebhookComponent implements OnInit {
sendCreate: webhookSchema.sendCreate,
sendUpdate: webhookSchema.sendUpdate,
sendDelete: webhookSchema.sendDelete,
sendPublish: webhookSchema.sendPublish,
sendUnpublish: webhookSchema.sendUnpublish
sendPublish: webhookSchema.sendPublish
});
} else {
return null;
@ -117,8 +115,7 @@ export class WebhookComponent implements OnInit {
sendCreate: false,
sendUpdate: false,
sendDelete: false,
sendPublish: false,
sendUnpublish: false
sendPublish: false
})).sortByStringAsc(x => x.schema.name);
this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name);
@ -135,8 +132,7 @@ export class WebhookComponent implements OnInit {
schema.sendCreate,
schema.sendUpdate,
schema.sendDelete,
schema.sendPublish,
schema.sendUnpublish)));
schema.sendPublish)));
this.emitUpdating(requestDto);
}
@ -163,8 +159,6 @@ export class WebhookComponent implements OnInit {
schemaForm.sendUpdate = value;
schemaForm.sendDelete = value;
schemaForm.sendPublish = value;
schemaForm.sendUnpublish = value;
return schemaForm;
}
@ -173,8 +167,7 @@ export class WebhookComponent implements OnInit {
schemaForm.sendCreate &&
schemaForm.sendUpdate &&
schemaForm.sendDelete &&
schemaForm.sendPublish &&
schemaForm.sendUnpublish;
schemaForm.sendPublish;
return schemaForm;
}

44
src/Squidex/app/shared/services/contents.service.spec.ts

@ -26,7 +26,7 @@ describe('ContentDto', () => {
const version = new Version('1');
it('should update data property and user info when updating', () => {
const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version);
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.update({ data: 2 }, modifier, modified);
expect(content_2.data).toEqual({ data: 2 });
@ -34,38 +34,38 @@ describe('ContentDto', () => {
expect(content_2.lastModifiedBy).toEqual(modifier);
});
it('should update isPublished property and user info when publishing', () => {
const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version);
it('should update status property and user info when publishing', () => {
const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.publish(modifier, modified);
expect(content_2.isPublished).toBeTruthy();
expect(content_2.status).toEqual('Published');
expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier);
});
it('should update isPublished property and user info when unpublishing', () => {
const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version);
it('should update status property and user info when unpublishing', () => {
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.unpublish(modifier, modified);
expect(content_2.isPublished).toBeFalsy();
expect(content_2.status).toEqual('Draft');
expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier);
});
it('should update isArchived property and user info when archiving', () => {
const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version);
it('should update status property and user info when archiving', () => {
const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.archive(modifier, modified);
expect(content_2.isArchived).toBeTruthy();
expect(content_2.status).toEqual('Archived');
expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier);
});
it('should update isArchived property and user info when restoring', () => {
const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version);
it('should update status property and user info when restoring', () => {
const content_1 = new ContentDto('1', 'Archived', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.restore(modifier, modified);
expect(content_2.isArchived).toBeFalsy();
expect(content_2.status).toEqual('Draft');
expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier);
});
@ -73,7 +73,7 @@ describe('ContentDto', () => {
it('should update data property when setting data', () => {
const newData = {};
const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version);
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.setData(newData);
expect(content_2.data).toBe(newData);
@ -119,7 +119,7 @@ describe('ContentsService', () => {
items: [
{
id: 'id1',
isPublished: true,
status: 'Published',
created: '2016-12-12T10:10',
createdBy: 'Created1',
lastModified: '2017-12-12T10:10',
@ -129,7 +129,7 @@ describe('ContentsService', () => {
},
{
id: 'id2',
isPublished: true,
status: 'Published',
created: '2016-10-12T10:10',
createdBy: 'Created2',
lastModified: '2017-10-12T10:10',
@ -142,12 +142,12 @@ describe('ContentsService', () => {
expect(contents).toEqual(
new ContentsDto(10, [
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1',
new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},
new Version('11')),
new ContentDto('id2', true, false, 'Created2', 'LastModifiedBy2',
new ContentDto('id2', 'Published', 'Created2', 'LastModifiedBy2',
DateTime.parseISO_UTC('2016-10-12T10:10'),
DateTime.parseISO_UTC('2017-10-12T10:10'),
{},
@ -222,7 +222,7 @@ describe('ContentsService', () => {
req.flush({
id: 'id1',
isPublished: true,
status: 'Published',
created: '2016-12-12T10:10',
createdBy: 'Created1',
lastModified: '2017-12-12T10:10',
@ -232,7 +232,7 @@ describe('ContentsService', () => {
});
expect(content).toEqual(
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1',
new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},
@ -280,7 +280,7 @@ describe('ContentsService', () => {
req.flush({
id: 'id1',
isPublished: true,
status: 'Published',
created: '2016-12-12T10:10',
createdBy: 'Created1',
lastModified: '2017-12-12T10:10',
@ -290,7 +290,7 @@ describe('ContentsService', () => {
});
expect(content).toEqual(
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1',
new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},

51
src/Squidex/app/shared/services/contents.service.ts

@ -30,8 +30,7 @@ export class ContentsDto {
export class ContentDto {
constructor(
public readonly id: string,
public readonly isPublished: boolean,
public readonly isArchived: boolean,
public readonly status: string,
public readonly createdBy: string,
public readonly lastModifiedBy: string,
public readonly created: DateTime,
@ -44,8 +43,7 @@ export class ContentDto {
public setData(data: any): ContentDto {
return new ContentDto(
this.id,
this.isPublished,
this.isArchived,
this.status,
this.createdBy,
this.lastModifiedBy,
this.created,
@ -55,43 +53,25 @@ export class ContentDto {
}
public publish(user: string, now?: DateTime): ContentDto {
return new ContentDto(
this.id,
true,
this.isArchived,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
this.version);
return this.changeStatus('Published', user, now);
}
public unpublish(user: string, now?: DateTime): ContentDto {
return new ContentDto(
this.id,
false,
this.isArchived,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
this.version);
return this.changeStatus('Draft', user, now);
}
public archive(user: string, now?: DateTime): ContentDto {
return new ContentDto(
this.id,
this.isPublished,
true,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
this.version);
return this.changeStatus('Archived', user, now);
}
public restore(user: string, now?: DateTime): ContentDto {
return this.changeStatus('Draft', user, now);
}
private changeStatus(status: string, user: string, now?: DateTime): ContentDto {
return new ContentDto(
this.id,
this.isPublished,
false,
status,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
@ -101,8 +81,7 @@ export class ContentDto {
public update(data: any, user: string, now?: DateTime): ContentDto {
return new ContentDto(
this.id,
this.isPublished,
this.isArchived,
this.status,
this.createdBy, user,
this.created, now || DateTime.now(),
data,
@ -159,8 +138,7 @@ export class ContentsService {
return new ContentsDto(response.total, items.map(item => {
return new ContentDto(
item.id,
item.isPublished,
item.isArchived === true,
item.status,
item.createdBy,
item.lastModifiedBy,
DateTime.parseISO_UTC(item.created),
@ -179,8 +157,7 @@ export class ContentsService {
.map(response => {
return new ContentDto(
response.id,
response.isPublished,
response.isArchived === true,
response.status,
response.createdBy,
response.lastModifiedBy,
DateTime.parseISO_UTC(response.created),
@ -209,7 +186,7 @@ export class ContentsService {
.map(response => {
return new ContentDto(
response.id,
response.isPublished, false,
response.status,
response.createdBy,
response.lastModifiedBy,
DateTime.parseISO_UTC(response.created),

14
src/Squidex/app/shared/services/webhooks.service.spec.ts

@ -34,8 +34,8 @@ describe('WebhookDto', () => {
const webhook_2 =
webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2',
[
new WebhookSchemaDto('1', true, true, true, true, true),
new WebhookSchemaDto('2', true, true, true, true, true)
new WebhookSchemaDto('1', true, true, true, true),
new WebhookSchemaDto('2', true, true, true, true)
]), modifier, modified);
expect(webhook_2.url).toEqual('http://squidex.io/hook2');
@ -99,15 +99,13 @@ describe('WebhooksService', () => {
sendCreate: true,
sendUpdate: true,
sendDelete: true,
sendPublish: true,
sendUnpublish: true
sendPublish: true
}, {
schemaId: '2',
sendCreate: true,
sendUpdate: true,
sendDelete: true,
sendPublish: true,
sendUnpublish: true
sendPublish: true
}]
}
]);
@ -118,8 +116,8 @@ describe('WebhooksService', () => {
DateTime.parseISO_UTC('2017-12-12T10:10'),
version,
[
new WebhookSchemaDto('1', true, true, true, true, true),
new WebhookSchemaDto('2', true, true, true, true, true)
new WebhookSchemaDto('1', true, true, true, true),
new WebhookSchemaDto('2', true, true, true, true)
],
'http://squidex.io/hook', 1, 2, 3, 4)
]);

6
src/Squidex/app/shared/services/webhooks.service.ts

@ -58,8 +58,7 @@ export class WebhookSchemaDto {
public readonly sendCreate: boolean,
public readonly sendUpdate: boolean,
public readonly sendDelete: boolean,
public readonly sendPublish: boolean,
public readonly sendUnpublish: boolean
public readonly sendPublish: boolean
) {
}
}
@ -125,8 +124,7 @@ export class WebhooksService {
schema.sendCreate,
schema.sendUpdate,
schema.sendDelete,
schema.sendPublish,
schema.sendUnpublish));
schema.sendPublish));
return new WebhookDto(
item.id,

34
tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs

@ -0,0 +1,34 @@
// ==========================================================================
// StatusFlowTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Xunit;
namespace Squidex.Domain.Apps.Core.Contents
{
public class StatusFlowTests
{
[Fact]
public void Should_make_tests()
{
Assert.True(StatusFlow.Exists(Status.Draft));
Assert.True(StatusFlow.Exists(Status.Archived));
Assert.True(StatusFlow.Exists(Status.Published));
Assert.True(StatusFlow.CanChange(Status.Draft, Status.Archived));
Assert.True(StatusFlow.CanChange(Status.Draft, Status.Published));
Assert.True(StatusFlow.CanChange(Status.Published, Status.Draft));
Assert.True(StatusFlow.CanChange(Status.Published, Status.Archived));
Assert.True(StatusFlow.CanChange(Status.Archived, Status.Draft));
Assert.False(StatusFlow.Exists((Status)int.MaxValue));
Assert.False(StatusFlow.CanChange(Status.Archived, Status.Published));
}
}
}

44
tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs

@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Xunit;
namespace Squidex.Domain.Apps.Read.Contents
@ -37,17 +38,20 @@ namespace Squidex.Domain.Apps.Read.Contents
private readonly Guid contentId = Guid.NewGuid();
private readonly NamedContentData data = new NamedContentData();
private readonly NamedContentData transformedData = new NamedContentData();
private readonly ClaimsPrincipal user = new ClaimsPrincipal();
private readonly ClaimsPrincipal user;
private readonly ClaimsIdentity identity = new ClaimsIdentity();
private readonly EdmModelBuilder modelBuilder = A.Fake<EdmModelBuilder>();
private readonly ContentQueryService sut;
public ContentQueryServiceTests()
{
user = new ClaimsPrincipal(identity);
A.CallTo(() => app.Id).Returns(appId);
A.CallTo(() => content.Id).Returns(contentId);
A.CallTo(() => content.Data).Returns(data);
A.CallTo(() => content.IsPublished).Returns(true);
A.CallTo(() => content.Status).Returns(Status.Published);
sut = new ContentQueryService(contentRepository, schemas, scriptEngine, modelBuilder);
}
@ -116,15 +120,43 @@ namespace Squidex.Domain.Apps.Read.Contents
}
[Fact]
public async Task Should_return_contents_from_repository_and_transform()
public async Task Should_return_non_archived_contents_from_repository_and_transform()
{
await TestManyRequest(true, false, Status.Draft, Status.Published);
}
[Fact]
public async Task Should_return_archived_contents_from_repository_and_transform()
{
await TestManyRequest(true, true, Status.Archived);
}
[Fact]
public async Task Should_return_draft_contents_from_repository_and_transform()
{
await TestManyRequest(false, false, Status.Published);
}
[Fact]
public async Task Should_return_draft_contents_from_repository_and_transform_when_requesting_archive_as_non_frontend()
{
await TestManyRequest(false, true, Status.Published);
}
private async Task TestManyRequest(bool isFrontend, bool archive, params Status[] status)
{
if (isFrontend)
{
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend"));
}
var ids = new HashSet<Guid>();
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false))
.Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, false, true, ids, A<ODataUriParser>.Ignored))
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids, A<ODataUriParser>.Ignored))
.Returns(new List<IContentEntity> { content });
A.CallTo(() => contentRepository.CountAsync(app, schema, false, true, ids, A<ODataUriParser>.Ignored))
A.CallTo(() => contentRepository.CountAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids, A<ODataUriParser>.Ignored))
.Returns(123);
A.CallTo(() => schema.ScriptQuery)
@ -133,7 +165,7 @@ namespace Squidex.Domain.Apps.Read.Contents
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "<query-script>"))
.Returns(transformedData);
var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, true, ids, null);
var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, ids, null);
Assert.Equal(123, result.Total);
Assert.Equal(schema, result.Schema);

13
tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs

@ -16,23 +16,18 @@ namespace Squidex.Domain.Apps.Read.Contents.TestData
public sealed class FakeContentEntity : IContentEntity
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public Instant Created { get; set; }
public long Version { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public long Version { get; set; }
public bool IsPublished { get; set; }
public bool IsArchived { get; set; }
public NamedContentData Data { get; set; }
public Status Status { get; set; }
}
}

60
tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs

@ -176,75 +176,21 @@ namespace Squidex.Domain.Apps.Write.Contents
}
[Fact]
public async Task Publish_should_publish_domain_object()
public async Task ChangeStatus_should_publish_domain_object()
{
A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>");
CreateContent();
var context = CreateContextForCommand(new PublishContent { ContentId = contentId, User = user });
var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "publish content")).MustHaveHappened();
}
[Fact]
public async Task Unpublish_should_unpublish_domain_object()
{
A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>");
CreateContent();
var context = CreateContextForCommand(new UnpublishContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "unpublish content")).MustHaveHappened();
}
[Fact]
public async Task Archive_should_archive_domain_object()
{
A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>");
CreateContent();
var context = CreateContextForCommand(new ArchiveContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "archive content")).MustHaveHappened();
}
[Fact]
public async Task Restore_should_restore_domain_object()
{
A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>");
CreateContent();
var context = CreateContextForCommand(new RestoreContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "restore content")).MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "change content status")).MustHaveHappened();
}
[Fact]

139
tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs

@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Write.Contents
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data }),
CreateContentEvent(new ContentPublished())
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
}
@ -195,148 +195,38 @@ namespace Squidex.Domain.Apps.Write.Contents
}
[Fact]
public void Publish_should_throw_exception_if_not_created()
public void ChangeStatus_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Publish(CreateContentCommand(new PublishContent()));
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
});
}
[Fact]
public void Publish_should_throw_exception_if_content_is_deleted()
public void ChangeStatus_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Publish(CreateContentCommand(new PublishContent()));
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
});
}
[Fact]
public void Publish_should_refresh_properties_and_create_events()
public void ChangeStatus_should_refresh_properties_and_create_events()
{
CreateContent();
sut.Publish(CreateContentCommand(new PublishContent()));
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published }));
Assert.True(sut.IsPublished);
Assert.Equal(Status.Published, sut.Status);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentPublished())
);
}
[Fact]
public void Unpublish_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Unpublish(CreateContentCommand(new UnpublishContent()));
});
}
[Fact]
public void Unpublish_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Unpublish(CreateContentCommand(new UnpublishContent()));
});
}
[Fact]
public void Unpublish_should_refresh_properties_and_create_events()
{
CreateContent();
PublishContent();
sut.Unpublish(CreateContentCommand(new UnpublishContent()));
Assert.False(sut.IsPublished);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUnpublished())
);
}
[Fact]
public void Archive_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Archive(CreateContentCommand(new ArchiveContent()));
});
}
[Fact]
public void Archive_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Archive(CreateContentCommand(new ArchiveContent()));
});
}
[Fact]
public void Archive_should_refresh_properties_and_create_events()
{
CreateContent();
sut.Archive(CreateContentCommand(new ArchiveContent()));
Assert.True(sut.IsArchived);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentArchived())
);
}
[Fact]
public void Restore_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Restore(CreateContentCommand(new RestoreContent()));
});
}
[Fact]
public void Restore_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Restore(CreateContentCommand(new RestoreContent()));
});
}
[Fact]
public void Restore_should_refresh_properties_and_create_events()
{
CreateContent();
ArchiveContent();
sut.Restore(CreateContentCommand(new RestoreContent()));
Assert.False(sut.IsArchived);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentRestored())
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
}
@ -390,16 +280,9 @@ namespace Squidex.Domain.Apps.Write.Contents
((IAggregate)sut).ClearUncommittedEvents();
}
private void PublishContent()
{
sut.Publish(CreateContentCommand(new PublishContent()));
((IAggregate)sut).ClearUncommittedEvents();
}
private void ArchiveContent()
private void ChangeStatus(Status status)
{
sut.Archive(CreateContentCommand(new ArchiveContent()));
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status }));
((IAggregate)sut).ClearUncommittedEvents();
}

2
tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs

@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Write.Contents
var eventData2 = new EventData();
var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId<Guid>(appId, "my-app") };
var event2 = new ContentPublished();
var event2 = new ContentStatusChanged();
var events = new List<StoredEvent>
{

Loading…
Cancel
Save