mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
75 changed files with 2226 additions and 240 deletions
@ -0,0 +1,32 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using NodaTime; |
||||
|
using Squidex.Infrastructure; |
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Comments |
||||
|
{ |
||||
|
public sealed class Comment |
||||
|
{ |
||||
|
public Guid Id { get; } |
||||
|
|
||||
|
public Instant Time { get; } |
||||
|
|
||||
|
public RefToken User { get; } |
||||
|
|
||||
|
public string Text { get; } |
||||
|
|
||||
|
public Comment(Guid id, Instant time, RefToken user, string text) |
||||
|
{ |
||||
|
Id = id; |
||||
|
Time = time; |
||||
|
Text = text; |
||||
|
User = user; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments.Commands |
||||
|
{ |
||||
|
public abstract class CommentsCommand : SquidexCommand, IAggregateCommand, IAppCommand |
||||
|
{ |
||||
|
public Guid CommentsId { get; set; } |
||||
|
|
||||
|
public NamedId<Guid> AppId { get; set; } |
||||
|
|
||||
|
Guid IAggregateCommand.AggregateId |
||||
|
{ |
||||
|
get { return CommentsId; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments.Commands |
||||
|
{ |
||||
|
public sealed class CreateComment : CommentsCommand |
||||
|
{ |
||||
|
public Guid CommentId { get; } = Guid.NewGuid(); |
||||
|
|
||||
|
public string Text { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments.Commands |
||||
|
{ |
||||
|
public sealed class DeleteComment : CommentsCommand |
||||
|
{ |
||||
|
public Guid CommentId { get; set; } |
||||
|
|
||||
|
public string Text { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments.Commands |
||||
|
{ |
||||
|
public sealed class UpdateComment : CommentsCommand |
||||
|
{ |
||||
|
public Guid CommentId { get; set; } |
||||
|
|
||||
|
public string Text { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Guards; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.State; |
||||
|
using Squidex.Domain.Apps.Events.Comments; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments |
||||
|
{ |
||||
|
public sealed class CommentsGrain : DomainObjectGrainBase<CommentsState>, ICommentGrain |
||||
|
{ |
||||
|
private readonly IStore<Guid> store; |
||||
|
private readonly List<Envelope<CommentsEvent>> events = new List<Envelope<CommentsEvent>>(); |
||||
|
private CommentsState snapshot = new CommentsState { Version = EtagVersion.Empty }; |
||||
|
private IPersistence persistence; |
||||
|
|
||||
|
public override CommentsState Snapshot |
||||
|
{ |
||||
|
get { return snapshot; } |
||||
|
} |
||||
|
|
||||
|
public CommentsGrain(IStore<Guid> store, ISemanticLog log) |
||||
|
: base(log) |
||||
|
{ |
||||
|
Guard.NotNull(store, nameof(store)); |
||||
|
|
||||
|
this.store = store; |
||||
|
} |
||||
|
|
||||
|
protected override void ApplyEvent(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
snapshot = new CommentsState { Version = snapshot.Version + 1 }; |
||||
|
|
||||
|
events.Add(@event.To<CommentsEvent>()); |
||||
|
} |
||||
|
|
||||
|
protected override void RestorePreviousSnapshot(CommentsState previousSnapshot, long previousVersion) |
||||
|
{ |
||||
|
snapshot = previousSnapshot; |
||||
|
} |
||||
|
|
||||
|
protected override Task ReadAsync(Type type, Guid id) |
||||
|
{ |
||||
|
persistence = store.WithEventSourcing<Guid>(GetType(), id, ApplyEvent); |
||||
|
|
||||
|
return persistence.ReadAsync(); |
||||
|
} |
||||
|
|
||||
|
protected override async Task WriteAsync(Envelope<IEvent>[] events, long previousVersion) |
||||
|
{ |
||||
|
if (events.Length > 0) |
||||
|
{ |
||||
|
await persistence.WriteEventsAsync(events); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override Task<object> ExecuteAsync(IAggregateCommand command) |
||||
|
{ |
||||
|
switch (command) |
||||
|
{ |
||||
|
case CreateComment createComment: |
||||
|
return UpsertAsync(createComment, c => |
||||
|
{ |
||||
|
GuardComments.CanCreate(c); |
||||
|
|
||||
|
Create(c); |
||||
|
|
||||
|
return EntityCreatedResult.Create(createComment.CommentId, Version); |
||||
|
}); |
||||
|
|
||||
|
case UpdateComment updateComment: |
||||
|
return UpsertAsync(updateComment, c => |
||||
|
{ |
||||
|
GuardComments.CanUpdate(events, c); |
||||
|
|
||||
|
Update(c); |
||||
|
}); |
||||
|
|
||||
|
case DeleteComment deleteComment: |
||||
|
return UpsertAsync(deleteComment, c => |
||||
|
{ |
||||
|
GuardComments.CanDelete(events, c); |
||||
|
|
||||
|
Delete(c); |
||||
|
}); |
||||
|
|
||||
|
default: |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Create(CreateComment command) |
||||
|
{ |
||||
|
RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); |
||||
|
} |
||||
|
|
||||
|
public void Update(UpdateComment command) |
||||
|
{ |
||||
|
RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); |
||||
|
} |
||||
|
|
||||
|
public void Delete(DeleteComment command) |
||||
|
{ |
||||
|
RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); |
||||
|
} |
||||
|
|
||||
|
public Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any) |
||||
|
{ |
||||
|
return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)version)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
using Squidex.Domain.Apps.Core.Comments; |
||||
|
using Squidex.Domain.Apps.Events.Comments; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments |
||||
|
{ |
||||
|
public sealed class CommentsResult |
||||
|
{ |
||||
|
public List<Comment> CreatedComments { get; set; } = new List<Comment>(); |
||||
|
|
||||
|
public List<Comment> UpdatedComments { get; set; } = new List<Comment>(); |
||||
|
|
||||
|
public List<Guid> DeletedComments { get; set; } = new List<Guid>(); |
||||
|
|
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
public static CommentsResult FromEvents(IEnumerable<Envelope<CommentsEvent>> events, long currentVersion, int lastVersion) |
||||
|
{ |
||||
|
var result = new CommentsResult { Version = currentVersion }; |
||||
|
|
||||
|
foreach (var @event in events.Skip(lastVersion < 0 ? 0 : lastVersion + 1)) |
||||
|
{ |
||||
|
switch (@event.Payload) |
||||
|
{ |
||||
|
case CommentDeleted deleted: |
||||
|
{ |
||||
|
var id = deleted.CommentId; |
||||
|
|
||||
|
if (result.CreatedComments.Any(x => x.Id == id)) |
||||
|
{ |
||||
|
result.CreatedComments.RemoveAll(x => x.Id == id); |
||||
|
} |
||||
|
else if (result.UpdatedComments.Any(x => x.Id == id)) |
||||
|
{ |
||||
|
result.UpdatedComments.RemoveAll(x => x.Id == id); |
||||
|
result.DeletedComments.Add(id); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
result.DeletedComments.Add(id); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case CommentCreated created: |
||||
|
{ |
||||
|
var comment = new Comment( |
||||
|
created.CommentId, |
||||
|
@event.Headers.Timestamp(), |
||||
|
@event.Payload.Actor, |
||||
|
created.Text); |
||||
|
|
||||
|
result.CreatedComments.Add(comment); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case CommentUpdated updated: |
||||
|
{ |
||||
|
var id = updated.CommentId; |
||||
|
|
||||
|
var comment = new Comment( |
||||
|
id, |
||||
|
@event.Headers.Timestamp(), |
||||
|
@event.Payload.Actor, |
||||
|
updated.Text); |
||||
|
|
||||
|
if (result.CreatedComments.Any(x => x.Id == id)) |
||||
|
{ |
||||
|
result.CreatedComments.RemoveAll(x => x.Id == id); |
||||
|
result.CreatedComments.Add(comment); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
result.UpdatedComments.Add(comment); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Domain.Apps.Events.Comments; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments.Guards |
||||
|
{ |
||||
|
public static class GuardComments |
||||
|
{ |
||||
|
public static void CanCreate(CreateComment command) |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
Validate.It(() => "Cannot create comment.", e => |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(command.Text)) |
||||
|
{ |
||||
|
e("Text is required.", nameof(command.Text)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static void CanUpdate(List<Envelope<CommentsEvent>> events, UpdateComment command) |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
var comment = FindComment(events, command.CommentId); |
||||
|
|
||||
|
if (!comment.Payload.Actor.Equals(command.Actor)) |
||||
|
{ |
||||
|
throw new DomainException("Comment is created by another actor."); |
||||
|
} |
||||
|
|
||||
|
Validate.It(() => "Cannot update comment.", e => |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(command.Text)) |
||||
|
{ |
||||
|
e("Text is required.", nameof(command.Text)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static void CanDelete(List<Envelope<CommentsEvent>> events, DeleteComment command) |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
var comment = FindComment(events, command.CommentId); |
||||
|
|
||||
|
if (!comment.Payload.Actor.Equals(command.Actor)) |
||||
|
{ |
||||
|
throw new DomainException("Comment is created by another actor."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static Envelope<CommentCreated> FindComment(List<Envelope<CommentsEvent>> events, Guid commentId) |
||||
|
{ |
||||
|
Envelope<CommentCreated> result = null; |
||||
|
|
||||
|
foreach (var @event in events) |
||||
|
{ |
||||
|
if (@event.Payload is CommentCreated created && created.CommentId == commentId) |
||||
|
{ |
||||
|
result = @event.To<CommentCreated>(); |
||||
|
} |
||||
|
else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) |
||||
|
{ |
||||
|
result = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (result == null) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(commentId.ToString(), "Comments", typeof(CommentsGrain)); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments |
||||
|
{ |
||||
|
public interface ICommentGrain : IDomainObjectGrain |
||||
|
{ |
||||
|
Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments.State |
||||
|
{ |
||||
|
public sealed class CommentsState : DomainObjectState<CommentsState> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Comments |
||||
|
{ |
||||
|
[EventType(nameof(CommentCreated))] |
||||
|
public sealed class CommentCreated : CommentsEvent |
||||
|
{ |
||||
|
public Guid CommentId { get; set; } |
||||
|
|
||||
|
public string Text { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Comments |
||||
|
{ |
||||
|
[EventType(nameof(CommentDeleted))] |
||||
|
public sealed class CommentDeleted : CommentsEvent |
||||
|
{ |
||||
|
public Guid CommentId { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Comments |
||||
|
{ |
||||
|
[EventType(nameof(CommentUpdated))] |
||||
|
public sealed class CommentUpdated : CommentsEvent |
||||
|
{ |
||||
|
public Guid CommentId { get; set; } |
||||
|
|
||||
|
public string Text { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Comments |
||||
|
{ |
||||
|
public abstract class CommentsEvent : AppEvent |
||||
|
{ |
||||
|
public Guid CommentsId { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Orleans; |
||||
|
using Squidex.Areas.Api.Controllers.Comments.Models; |
||||
|
using Squidex.Domain.Apps.Entities.Comments; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Pipeline; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Comments |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Manages comments for any kind of resource.
|
||||
|
/// </summary>
|
||||
|
[ApiAuthorize] |
||||
|
[ApiExceptionFilter] |
||||
|
[AppApi] |
||||
|
[ApiExplorerSettings(GroupName = nameof(Comments))] |
||||
|
public sealed class CommentsController : ApiController |
||||
|
{ |
||||
|
private readonly IGrainFactory grainFactory; |
||||
|
|
||||
|
public CommentsController(ICommandBus commandBus, IGrainFactory grainFactory) |
||||
|
: base(commandBus) |
||||
|
{ |
||||
|
this.grainFactory = grainFactory; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get all comments.
|
||||
|
/// </summary>
|
||||
|
/// <param name="app">The name of the app.</param>
|
||||
|
/// <param name="commentsId">The id of the comments.</param>
|
||||
|
/// <returns>
|
||||
|
/// 200 => All comments returned.
|
||||
|
/// 404 => App not found.
|
||||
|
/// </returns>
|
||||
|
[HttpGet] |
||||
|
[Route("apps/{app}/comments/{commentsId}")] |
||||
|
[ProducesResponseType(typeof(CommentsDto), 200)] |
||||
|
[ApiCosts(0)] |
||||
|
public async Task<IActionResult> GetComments(string app, Guid commentsId) |
||||
|
{ |
||||
|
if (!long.TryParse(Request.Headers["If-None-Match"], out var version)) |
||||
|
{ |
||||
|
version = EtagVersion.Any; |
||||
|
} |
||||
|
|
||||
|
var result = await grainFactory.GetGrain<ICommentGrain>(commentsId).GetCommentsAsync(version); |
||||
|
var response = CommentsDto.FromResult(result); |
||||
|
|
||||
|
Response.Headers["ETag"] = response.Version.ToString(); |
||||
|
|
||||
|
return Ok(response); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Create a new comment.
|
||||
|
/// </summary>
|
||||
|
/// <param name="app">The name of the app.</param>
|
||||
|
/// <param name="commentsId">The id of the comments.</param>
|
||||
|
/// <param name="request">The comment object that needs to created.</param>
|
||||
|
/// <returns>
|
||||
|
/// 201 => Comment created.
|
||||
|
/// 400 => Comment is not valid.
|
||||
|
/// 404 => App not found.
|
||||
|
/// </returns>
|
||||
|
[HttpPost] |
||||
|
[Route("apps/{app}/comments/{commentsId}")] |
||||
|
[ProducesResponseType(typeof(EntityCreatedDto), 201)] |
||||
|
[ProducesResponseType(typeof(ErrorDto), 400)] |
||||
|
[ApiCosts(0)] |
||||
|
public async Task<IActionResult> PostComment(string app, Guid commentsId, [FromBody] UpsertCommentDto request) |
||||
|
{ |
||||
|
var command = request.ToCreateCommand(commentsId); |
||||
|
var context = await CommandBus.PublishAsync(command); |
||||
|
|
||||
|
var response = CommentDto.FromCommand(command); |
||||
|
|
||||
|
return CreatedAtAction(nameof(GetComments), new { commentsId }, response); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates the comment.
|
||||
|
/// </summary>
|
||||
|
/// <param name="app">The name of the app.</param>
|
||||
|
/// <param name="commentsId">The id of the comments.</param>
|
||||
|
/// <param name="commentId">The id of the comment.</param>
|
||||
|
/// <param name="request">The comment object that needs to updated.</param>
|
||||
|
/// <returns>
|
||||
|
/// 204 => Comment updated.
|
||||
|
/// 400 => Comment text not valid.
|
||||
|
/// 404 => Comment or app not found.
|
||||
|
/// </returns>
|
||||
|
[MustBeAppReader] |
||||
|
[HttpPut] |
||||
|
[Route("apps/{app}/comments/{commentsId}/{commentId}")] |
||||
|
[ProducesResponseType(typeof(ErrorDto), 400)] |
||||
|
[ApiCosts(0)] |
||||
|
public async Task<IActionResult> PutComment(string app, Guid commentsId, Guid commentId, [FromBody] UpsertCommentDto request) |
||||
|
{ |
||||
|
await CommandBus.PublishAsync(request.ToUpdateComment(commentsId, commentId)); |
||||
|
|
||||
|
return NoContent(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Deletes the comment.
|
||||
|
/// </summary>
|
||||
|
/// <param name="app">The name of the app.</param>
|
||||
|
/// <param name="commentsId">The id of the comments.</param>
|
||||
|
/// <param name="commentId">The id of the comment.</param>
|
||||
|
/// <returns>
|
||||
|
/// 204 => Comment deleted.
|
||||
|
/// 404 => Comment or app not found.
|
||||
|
/// </returns>
|
||||
|
[HttpDelete] |
||||
|
[Route("apps/{app}/comments/{commentsId}/{commentId}")] |
||||
|
[ProducesResponseType(typeof(ErrorDto), 400)] |
||||
|
[ApiCosts(0)] |
||||
|
public async Task<IActionResult> DeleteComment(string app, Guid commentsId, Guid commentId) |
||||
|
{ |
||||
|
await CommandBus.PublishAsync(new DeleteComment { CommentsId = commentsId, CommentId = commentId }); |
||||
|
|
||||
|
return NoContent(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Comments; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Comments.Models |
||||
|
{ |
||||
|
public sealed class CommentDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The id of the comment.
|
||||
|
/// </summary>
|
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The time when the comment was created or updated last.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public Instant Time { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The user who created or updated the comment.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public RefToken User { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The text of the comment.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Text { get; set; } |
||||
|
|
||||
|
public static CommentDto FromComment(Comment comment) |
||||
|
{ |
||||
|
return SimpleMapper.Map(comment, new CommentDto()); |
||||
|
} |
||||
|
|
||||
|
public static CommentDto FromCommand(CreateComment command) |
||||
|
{ |
||||
|
return SimpleMapper.Map(command, new CommentDto { Id = command.CommentId, User = command.Actor, Time = SystemClock.Instance.GetCurrentInstant() }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
using Squidex.Domain.Apps.Entities.Comments; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Comments.Models |
||||
|
{ |
||||
|
public sealed class CommentsDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The created comments including the updates.
|
||||
|
/// </summary>
|
||||
|
public List<CommentDto> CreatedComments { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The updates comments since the last version.
|
||||
|
/// </summary>
|
||||
|
public List<CommentDto> UpdatedComments { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The deleted comments since the last version.
|
||||
|
/// </summary>
|
||||
|
public List<Guid> DeletedComments { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The current version.
|
||||
|
/// </summary>
|
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
public static CommentsDto FromResult(CommentsResult result) |
||||
|
{ |
||||
|
return new CommentsDto |
||||
|
{ |
||||
|
CreatedComments = result.CreatedComments.Select(CommentDto.FromComment).ToList(), |
||||
|
UpdatedComments = result.UpdatedComments.Select(CommentDto.FromComment).ToList(), |
||||
|
DeletedComments = result.DeletedComments, |
||||
|
Version = result.Version |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Comments.Models |
||||
|
{ |
||||
|
public sealed class UpsertCommentDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The comment text.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Text { get; set; } |
||||
|
|
||||
|
public CreateComment ToCreateCommand(Guid commentsId) |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new CreateComment { CommentsId = commentsId }); |
||||
|
} |
||||
|
|
||||
|
public UpdateComment ToUpdateComment(Guid commentsId, Guid commentId) |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new UpdateComment { CommentsId = commentsId, CommentId = commentId }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
<sqx-comments [commentsId]="commentsId"></sqx-comments> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
@ -0,0 +1,30 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
|
||||
|
import { allParams } from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-comments-page', |
||||
|
styleUrls: ['./comments-page.component.scss'], |
||||
|
templateUrl: './comments-page.component.html' |
||||
|
}) |
||||
|
export class CommentsPageComponent implements OnInit { |
||||
|
public commentsId: string; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly route: ActivatedRoute |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.commentsId = allParams(this.route)['contentId']; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,19 @@ |
|||||
|
<div class="comment row no-gutters"> |
||||
|
<div class="col col-auto"> |
||||
|
<img class="user-picture" [attr.title]="comment.user | sqxUserNameRef:null" [attr.src]="comment.user | sqxUserPictureRef" /> |
||||
|
</div> |
||||
|
<div class="col pl-2"> |
||||
|
<div class="comment-message"> |
||||
|
<div class="user-row"> |
||||
|
<div class="user-ref">{{comment.user | sqxUserNameRef:null}}</div> |
||||
|
|
||||
|
<button *ngIf="comment.user === userId" type="button" class="btn btn-sm btn-link btn-danger item-remove" (click)="deleting.emit()!"> |
||||
|
<i class="icon-bin2"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div>{{comment.text}}</div> |
||||
|
<div class="comment-created text-muted">{{comment.time | sqxFromNow}}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,43 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.user-ref { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.item-remove { |
||||
|
@include absolute(-5px, -15px, auto, auto); |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.user-row { |
||||
|
& { |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
.item-remove { |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.comment { |
||||
|
& { |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
margin-bottom: .75rem; |
||||
|
} |
||||
|
|
||||
|
&-message { |
||||
|
margin-bottom: .375rem; |
||||
|
} |
||||
|
|
||||
|
&-created { |
||||
|
font-size: .75rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.text-muted { |
||||
|
color: $color-history; |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
||||
|
import { FormBuilder } from '@angular/forms'; |
||||
|
|
||||
|
import { CommentDto, UpsertCommentForm } from '@app/shared/internal'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-comment', |
||||
|
styleUrls: ['./comment.component.scss'], |
||||
|
templateUrl: './comment.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class CommentComponent { |
||||
|
public editForm = new UpsertCommentForm(this.formBuilder); |
||||
|
|
||||
|
@Input() |
||||
|
public comment: CommentDto; |
||||
|
|
||||
|
@Input() |
||||
|
public userId: string; |
||||
|
|
||||
|
@Output() |
||||
|
public deleting = new EventEmitter(); |
||||
|
|
||||
|
@Output() |
||||
|
public updated = new EventEmitter<string>(); |
||||
|
|
||||
|
constructor( |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false" contentClass="grid"> |
||||
|
<ng-container title> |
||||
|
Comments |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<div class="grid-content" #scrollMe [scrollTop]="scrollMe.scrollHeight"> |
||||
|
<sqx-comment *ngFor="let comment of state.comments | async; trackBy: trackByComment" |
||||
|
[comment]="comment" |
||||
|
[userId]="userId" |
||||
|
(updated)="update(comment, $event)" |
||||
|
(deleting)="delete(comment)"> |
||||
|
</sqx-comment> |
||||
|
</div> |
||||
|
|
||||
|
<div class="grid-footer"> |
||||
|
<form [formGroup]="commentForm.form" (submit)="comment()"> |
||||
|
<input class="form-control" name="text" formControlName="text" placeholder="Create a comment" /> |
||||
|
</form> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</sqx-panel> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,11 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.grid-footer { |
||||
|
border-top-width: 1px; |
||||
|
} |
||||
|
|
||||
|
.grid-body, |
||||
|
.grid-footer { |
||||
|
padding: 1rem; |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { FormBuilder } from '@angular/forms'; |
||||
|
import { Subscription, timer } from 'rxjs'; |
||||
|
import { onErrorResumeNext, switchMap } from 'rxjs/operators'; |
||||
|
|
||||
|
import { |
||||
|
AppsState, |
||||
|
AuthService, |
||||
|
CommentDto, |
||||
|
CommentsService, |
||||
|
CommentsState, |
||||
|
DialogService, |
||||
|
UpsertCommentForm |
||||
|
} from '@app/shared/internal'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-comments', |
||||
|
styleUrls: ['./comments.component.scss'], |
||||
|
templateUrl: './comments.component.html' |
||||
|
}) |
||||
|
export class CommentsComponent implements OnDestroy, OnInit { |
||||
|
private timer: Subscription; |
||||
|
|
||||
|
public state: CommentsState; |
||||
|
|
||||
|
public userId: string; |
||||
|
|
||||
|
public commentForm = new UpsertCommentForm(this.formBuilder); |
||||
|
|
||||
|
@Input() |
||||
|
public commentsId: string; |
||||
|
|
||||
|
constructor(authService: AuthService, |
||||
|
private readonly appsState: AppsState, |
||||
|
private readonly commentsService: CommentsService, |
||||
|
private readonly dialogs: DialogService, |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
this.userId = authService.user!.token; |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.timer.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.state = new CommentsState(this.appsState, this.commentsId, this.commentsService, this.dialogs); |
||||
|
|
||||
|
this.timer = timer(0, 4000).pipe(switchMap(() => this.state.load()), onErrorResumeNext()).subscribe(); |
||||
|
} |
||||
|
|
||||
|
public delete(comment: CommentDto) { |
||||
|
this.state.delete(comment.id).pipe(onErrorResumeNext()).subscribe(); |
||||
|
} |
||||
|
|
||||
|
public update(comment: CommentDto, text: string) { |
||||
|
this.state.update(comment.id, text).pipe(onErrorResumeNext()).subscribe(); |
||||
|
} |
||||
|
|
||||
|
public comment() { |
||||
|
const value = this.commentForm.submit(); |
||||
|
|
||||
|
if (value) { |
||||
|
this.state.create(value.text).pipe(onErrorResumeNext()).subscribe(); |
||||
|
|
||||
|
this.commentForm.submitCompleted({}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public trackByComment(index: number, comment: CommentDto) { |
||||
|
return comment.id; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,152 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
||||
|
import { inject, TestBed } from '@angular/core/testing'; |
||||
|
|
||||
|
import { |
||||
|
ApiUrlConfig, |
||||
|
CommentDto, |
||||
|
CommentsDto, |
||||
|
CommentsService, |
||||
|
DateTime, |
||||
|
UpsertCommentDto, |
||||
|
Version |
||||
|
} from './../'; |
||||
|
|
||||
|
describe('CommentsService', () => { |
||||
|
const user = 'me'; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
TestBed.configureTestingModule({ |
||||
|
imports: [ |
||||
|
HttpClientTestingModule |
||||
|
], |
||||
|
providers: [ |
||||
|
CommentsService, |
||||
|
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') } |
||||
|
] |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
||||
|
httpMock.verify(); |
||||
|
})); |
||||
|
|
||||
|
it('should make get request to get comments', |
||||
|
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { |
||||
|
|
||||
|
let comments: CommentsDto; |
||||
|
|
||||
|
commentsService.getComments('my-app', 'my-comments', new Version('123')).subscribe(result => { |
||||
|
comments = result; |
||||
|
}); |
||||
|
|
||||
|
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments'); |
||||
|
|
||||
|
expect(req.request.method).toEqual('GET'); |
||||
|
expect(req.request.headers.get('If-None-Match')).toBe('123'); |
||||
|
|
||||
|
req.flush({ |
||||
|
createdComments: [{ |
||||
|
id: '123', |
||||
|
text: 'text1', |
||||
|
time: '2016-10-12T10:10', |
||||
|
user: user |
||||
|
}], |
||||
|
updatedComments: [{ |
||||
|
id: '456', |
||||
|
text: 'text2', |
||||
|
time: '2017-11-12T12:12', |
||||
|
user: user |
||||
|
}], |
||||
|
deletedComments: ['789'], |
||||
|
version: '9' |
||||
|
}); |
||||
|
|
||||
|
expect(comments!).toEqual( |
||||
|
new CommentsDto( |
||||
|
[ |
||||
|
new CommentDto('123', DateTime.parseISO_UTC('2016-10-12T10:10'), 'text1', user) |
||||
|
], [ |
||||
|
new CommentDto('456', DateTime.parseISO_UTC('2017-11-12T12:12'), 'text2', user) |
||||
|
], [ |
||||
|
'789' |
||||
|
], |
||||
|
new Version('9')) |
||||
|
); |
||||
|
})); |
||||
|
|
||||
|
it('should make get request to get comments and return empty result for 304', |
||||
|
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { |
||||
|
|
||||
|
let comments: CommentsDto; |
||||
|
|
||||
|
commentsService.getComments('my-app', 'my-comments', new Version('123')).subscribe(result => { |
||||
|
comments = result; |
||||
|
}); |
||||
|
|
||||
|
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments'); |
||||
|
|
||||
|
expect(req.request.method).toEqual('GET'); |
||||
|
expect(req.request.headers.get('If-None-Match')).toBe('123'); |
||||
|
|
||||
|
req.flush({}, { status: 304, statusText: 'NotModified' }); |
||||
|
|
||||
|
expect(comments!).toEqual(new CommentsDto([], [], [], new Version('123'))); |
||||
|
})); |
||||
|
|
||||
|
it('should make post request to create comment', |
||||
|
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { |
||||
|
|
||||
|
let comment: CommentDto; |
||||
|
|
||||
|
commentsService.postComment('my-app', 'my-comments', new UpsertCommentDto('text1')).subscribe(result => { |
||||
|
comment = <CommentDto>result; |
||||
|
}); |
||||
|
|
||||
|
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments'); |
||||
|
|
||||
|
expect(req.request.method).toEqual('POST'); |
||||
|
expect(req.request.headers.get('If-Match')).toBeNull(); |
||||
|
|
||||
|
req.flush({ |
||||
|
id: '123', |
||||
|
text: 'text1', |
||||
|
time: '2016-10-12T10:10', |
||||
|
user: user |
||||
|
}); |
||||
|
|
||||
|
expect(comment!).toEqual(new CommentDto('123', DateTime.parseISO_UTC('2016-10-12T10:10'), 'text1', user)); |
||||
|
})); |
||||
|
|
||||
|
it('should make put request to replace comment content', |
||||
|
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { |
||||
|
|
||||
|
commentsService.putComment('my-app', 'my-comments', '123', new UpsertCommentDto('text1')).subscribe(); |
||||
|
|
||||
|
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments/123'); |
||||
|
|
||||
|
expect(req.request.method).toEqual('PUT'); |
||||
|
expect(req.request.headers.get('If-Match')).toBeNull(); |
||||
|
|
||||
|
req.flush({}); |
||||
|
})); |
||||
|
|
||||
|
it('should make delete request to delete comment', |
||||
|
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { |
||||
|
|
||||
|
commentsService.deleteComment('my-app', 'my-comments', '123').subscribe(); |
||||
|
|
||||
|
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments/123'); |
||||
|
|
||||
|
expect(req.request.method).toEqual('DELETE'); |
||||
|
expect(req.request.headers.get('If-Match')).toBeNull(); |
||||
|
|
||||
|
req.flush({}); |
||||
|
})); |
||||
|
}); |
||||
@ -0,0 +1,136 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http'; |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { Observable, of, throwError } from 'rxjs'; |
||||
|
import { catchError, map } from 'rxjs/operators'; |
||||
|
|
||||
|
import { |
||||
|
ApiUrlConfig, |
||||
|
DateTime, |
||||
|
Model, |
||||
|
pretifyError, |
||||
|
Types, |
||||
|
Version |
||||
|
} from '@app/framework'; |
||||
|
|
||||
|
export class CommentsDto extends Model { |
||||
|
constructor( |
||||
|
public readonly createdComments: CommentDto[], |
||||
|
public readonly updatedComments: CommentDto[], |
||||
|
public readonly deletedComments: string[], |
||||
|
public readonly version: Version |
||||
|
) { |
||||
|
super(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class CommentDto extends Model { |
||||
|
constructor( |
||||
|
public readonly id: string, |
||||
|
public readonly time: DateTime, |
||||
|
public readonly text: string, |
||||
|
public readonly user: string |
||||
|
) { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
public with(value: Partial<CommentDto>): CommentDto { |
||||
|
return this.clone(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class UpsertCommentDto { |
||||
|
constructor( |
||||
|
public readonly text: string |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class CommentsService { |
||||
|
constructor( |
||||
|
private readonly http: HttpClient, |
||||
|
private readonly apiUrl: ApiUrlConfig |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public getComments(appName: string, commentsId: string, version: Version): Observable<CommentsDto> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}`); |
||||
|
|
||||
|
const options = { |
||||
|
headers: new HttpHeaders().set('If-None-Match', version.value) |
||||
|
}; |
||||
|
|
||||
|
return this.http.get(url, options).pipe( |
||||
|
catchError(err => { |
||||
|
if (err.status === 304) { |
||||
|
return of(new CommentsDto([], [], [], version)); |
||||
|
} |
||||
|
|
||||
|
return throwError(err); |
||||
|
}), |
||||
|
map(response => { |
||||
|
if (Types.is(response, CommentsDto)) { |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
const body: any = response; |
||||
|
|
||||
|
return new CommentsDto( |
||||
|
body.createdComments.map((item: any) => { |
||||
|
return new CommentDto( |
||||
|
item.id, |
||||
|
DateTime.parseISO_UTC(item.time), |
||||
|
item.text, |
||||
|
item.user); |
||||
|
}), |
||||
|
body.updatedComments.map((item: any) => { |
||||
|
return new CommentDto( |
||||
|
item.id, |
||||
|
DateTime.parseISO_UTC(item.time), |
||||
|
item.text, |
||||
|
item.user); |
||||
|
}), |
||||
|
body.deletedComments, |
||||
|
new Version(body.version) |
||||
|
); |
||||
|
}), |
||||
|
pretifyError('Failed to load comments.')); |
||||
|
} |
||||
|
|
||||
|
public postComment(appName: string, commentsId: string, dto: UpsertCommentDto): Observable<CommentDto> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}`); |
||||
|
|
||||
|
return this.http.post(url, dto).pipe( |
||||
|
map(response => { |
||||
|
const body: any = response; |
||||
|
|
||||
|
return new CommentDto( |
||||
|
body.id, |
||||
|
DateTime.parseISO_UTC(body.time), |
||||
|
body.text, |
||||
|
body.user); |
||||
|
}), |
||||
|
pretifyError('Failed to create comment.')); |
||||
|
} |
||||
|
|
||||
|
public putComment(appName: string, commentsId: string, commentId: string, dto: UpsertCommentDto): Observable<any> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}/${commentId}`); |
||||
|
|
||||
|
return this.http.put(url, dto).pipe( |
||||
|
pretifyError('Failed to update comment.')); |
||||
|
} |
||||
|
|
||||
|
public deleteComment(appName: string, commentsId: string, commentId: string): Observable<any> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}/${commentId}`); |
||||
|
|
||||
|
return this.http.delete(url).pipe( |
||||
|
pretifyError('Failed to delete comment.')); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
|
||||
|
import { Form } from '@app/framework'; |
||||
|
|
||||
|
export class UpsertCommentForm extends Form<FormGroup> { |
||||
|
constructor(formBuilder: FormBuilder) { |
||||
|
super(formBuilder.group({ |
||||
|
text: ['', |
||||
|
[ |
||||
|
Validators.required |
||||
|
] |
||||
|
] |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { of } from 'rxjs'; |
||||
|
import { IMock, It, Mock, Times } from 'typemoq'; |
||||
|
|
||||
|
import { |
||||
|
AppsState, |
||||
|
CommentDto, |
||||
|
CommentsDto, |
||||
|
CommentsService, |
||||
|
CommentsState, |
||||
|
DateTime, |
||||
|
DialogService, |
||||
|
ImmutableArray, |
||||
|
UpsertCommentDto, |
||||
|
Version |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
describe('CommentsState', () => { |
||||
|
const app = 'my-app'; |
||||
|
const commentsId = 'my-comments'; |
||||
|
const now = DateTime.today(); |
||||
|
const user = 'not-me'; |
||||
|
|
||||
|
const oldComments = new CommentsDto([ |
||||
|
new CommentDto('1', now, 'text1', user), |
||||
|
new CommentDto('2', now, 'text2', user) |
||||
|
], [], [], new Version('1')); |
||||
|
|
||||
|
let dialogs: IMock<DialogService>; |
||||
|
let appsState: IMock<AppsState>; |
||||
|
let commentsService: IMock<CommentsService>; |
||||
|
let commentsState: CommentsState; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
dialogs = Mock.ofType<DialogService>(); |
||||
|
|
||||
|
appsState = Mock.ofType<AppsState>(); |
||||
|
|
||||
|
appsState.setup(x => x.appName) |
||||
|
.returns(() => app); |
||||
|
|
||||
|
commentsService = Mock.ofType<CommentsService>(); |
||||
|
|
||||
|
commentsService.setup(x => x.getComments(app, commentsId, new Version(''))) |
||||
|
.returns(() => of(oldComments)); |
||||
|
|
||||
|
commentsState = new CommentsState(appsState.object, commentsId, commentsService.object, dialogs.object); |
||||
|
commentsState.load().subscribe(); |
||||
|
}); |
||||
|
|
||||
|
it('should load and merge comments', () => { |
||||
|
const newComments = new CommentsDto([ |
||||
|
new CommentDto('3', now, 'text3', user) |
||||
|
], [ |
||||
|
new CommentDto('2', now, 'text2_2', user) |
||||
|
], ['1'], new Version('2')); |
||||
|
|
||||
|
commentsService.setup(x => x.getComments(app, commentsId, new Version('1'))) |
||||
|
.returns(() => of(newComments)); |
||||
|
|
||||
|
commentsState.load().subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.isLoaded).toBeTruthy(); |
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('2', now, 'text2_2', user), |
||||
|
new CommentDto('3', now, 'text3', user) |
||||
|
])); |
||||
|
|
||||
|
commentsService.verify(x => x.getComments(app, commentsId, It.isAny()), Times.exactly(2)); |
||||
|
}); |
||||
|
|
||||
|
it('should add comment to snapshot when created', () => { |
||||
|
const newComment = new CommentDto('3', now, 'text3', user); |
||||
|
|
||||
|
commentsService.setup(x => x.postComment(app, commentsId, new UpsertCommentDto('text3'))) |
||||
|
.returns(() => of(newComment)); |
||||
|
|
||||
|
commentsState.create('text3').subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('1', now, 'text1', user), |
||||
|
new CommentDto('2', now, 'text2', user), |
||||
|
new CommentDto('3', now, 'text3', user) |
||||
|
])); |
||||
|
}); |
||||
|
|
||||
|
it('should update properties when updated', () => { |
||||
|
commentsService.setup(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2'))) |
||||
|
.returns(() => of({})); |
||||
|
|
||||
|
commentsState.update('2', 'text2_2', now).subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('1', now, 'text1', user), |
||||
|
new CommentDto('2', now, 'text2_2', user) |
||||
|
])); |
||||
|
|
||||
|
commentsService.verify(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2')), Times.once()); |
||||
|
}); |
||||
|
|
||||
|
it('should remove comment from snapshot when deleted', () => { |
||||
|
commentsService.setup(x => x.deleteComment(app, commentsId, '2')) |
||||
|
.returns(() => of({})); |
||||
|
|
||||
|
commentsState.delete('2').subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('1', now, 'text1', user) |
||||
|
])); |
||||
|
|
||||
|
commentsService.verify(x => x.deleteComment(app, commentsId, '2'), Times.once()); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,128 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; |
||||
|
|
||||
|
import { |
||||
|
DateTime, |
||||
|
DialogService, |
||||
|
ImmutableArray, |
||||
|
notify, |
||||
|
State, |
||||
|
Version |
||||
|
} from '@app/framework'; |
||||
|
|
||||
|
import { |
||||
|
CommentDto, |
||||
|
CommentsService, |
||||
|
UpsertCommentDto |
||||
|
} from './../services/comments.service'; |
||||
|
|
||||
|
import { AppsState } from './apps.state'; |
||||
|
|
||||
|
interface Snapshot { |
||||
|
comments: ImmutableArray<CommentDto>; |
||||
|
|
||||
|
version: Version; |
||||
|
|
||||
|
isLoaded?: boolean; |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class CommentsState extends State<Snapshot> { |
||||
|
public comments = |
||||
|
this.changes.pipe(map(x => x.comments), |
||||
|
distinctUntilChanged()); |
||||
|
|
||||
|
public isLoaded = |
||||
|
this.changes.pipe(map(x => !!x.isLoaded), |
||||
|
distinctUntilChanged()); |
||||
|
|
||||
|
constructor( |
||||
|
private readonly appsState: AppsState, |
||||
|
private readonly commentsId: string, |
||||
|
private readonly commentsService: CommentsService, |
||||
|
private readonly dialogs: DialogService |
||||
|
) { |
||||
|
super({ comments: ImmutableArray.empty(), version: new Version('') }); |
||||
|
} |
||||
|
|
||||
|
public load(): Observable<any> { |
||||
|
return this.commentsService.getComments(this.appName, this.commentsId, this.version).pipe( |
||||
|
tap(dtos => { |
||||
|
this.next(s => { |
||||
|
let comments = s.comments; |
||||
|
|
||||
|
for (let created of dtos.createdComments) { |
||||
|
if (!comments.find(x => x.id === created.id)) { |
||||
|
comments = comments.push(created); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (let updated of dtos.updatedComments) { |
||||
|
comments = comments.replaceBy('id', updated); |
||||
|
} |
||||
|
|
||||
|
for (let deleted of dtos.deletedComments) { |
||||
|
comments = comments.filter(x => x.id !== deleted); |
||||
|
} |
||||
|
|
||||
|
return { ...s, comments, isLoaded: true, version: dtos.version }; |
||||
|
}); |
||||
|
}), |
||||
|
notify(this.dialogs)); |
||||
|
} |
||||
|
|
||||
|
public create(text: string): Observable<any> { |
||||
|
return this.commentsService.postComment(this.appName, this.commentsId, new UpsertCommentDto(text)).pipe( |
||||
|
tap(dto => { |
||||
|
this.next(s => { |
||||
|
const comments = s.comments.push(dto); |
||||
|
|
||||
|
return { ...s, comments }; |
||||
|
}); |
||||
|
}), |
||||
|
notify(this.dialogs)); |
||||
|
} |
||||
|
|
||||
|
public update(commentId: string, text: string, now?: DateTime): Observable<any> { |
||||
|
return this.commentsService.putComment(this.appName, this.commentsId, commentId, new UpsertCommentDto(text)).pipe( |
||||
|
tap(() => { |
||||
|
this.next(s => { |
||||
|
const comments = s.comments.map(c => c.id === commentId ? update(c, text, now || DateTime.now()) : c); |
||||
|
|
||||
|
return { ...s, comments }; |
||||
|
}); |
||||
|
}), |
||||
|
notify(this.dialogs)); |
||||
|
} |
||||
|
|
||||
|
public delete(commentId: string): Observable<any> { |
||||
|
return this.commentsService.deleteComment(this.appName, this.commentsId, commentId).pipe( |
||||
|
tap(() => { |
||||
|
this.next(s => { |
||||
|
const comments = s.comments.filter(c => c.id !== commentId); |
||||
|
|
||||
|
return { ...s, comments }; |
||||
|
}); |
||||
|
}), |
||||
|
notify(this.dialogs)); |
||||
|
} |
||||
|
|
||||
|
private get version() { |
||||
|
return this.snapshot.version; |
||||
|
} |
||||
|
|
||||
|
private get appName() { |
||||
|
return this.appsState.appName; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const update = (comment: CommentDto, text: string, time: DateTime) => |
||||
|
comment.with({ text, time }); |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -0,0 +1,162 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using FluentAssertions; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Comments; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.State; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Events.Comments; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments |
||||
|
{ |
||||
|
public class CommentsGrainTests : HandlerTestBase<CommentsGrain, CommentsState> |
||||
|
{ |
||||
|
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
||||
|
private readonly Guid commentsId = Guid.NewGuid(); |
||||
|
private readonly CommentsGrain sut; |
||||
|
|
||||
|
protected override Guid Id |
||||
|
{ |
||||
|
get { return commentsId; } |
||||
|
} |
||||
|
|
||||
|
public CommentsGrainTests() |
||||
|
{ |
||||
|
sut = new CommentsGrain(Store, A.Dummy<ISemanticLog>()); |
||||
|
sut.OnActivateAsync(Id).Wait(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_create_events() |
||||
|
{ |
||||
|
var command = new CreateComment { Text = "text1" }; |
||||
|
|
||||
|
var result = await sut.ExecuteAsync(CreateCommentsCommand(command)); |
||||
|
|
||||
|
result.ShouldBeEquivalent(EntityCreatedResult.Create(command.CommentId, 0)); |
||||
|
|
||||
|
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult { Version = 0 }); |
||||
|
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult |
||||
|
{ |
||||
|
CreatedComments = new List<Comment> |
||||
|
{ |
||||
|
new Comment(command.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), command.Actor, "text1") |
||||
|
}, |
||||
|
Version = 0 |
||||
|
}); |
||||
|
|
||||
|
LastEvents |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateCommentsEvent(new CommentCreated { CommentId = command.CommentId, Text = command.Text }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Update_should_create_events_and_update_state() |
||||
|
{ |
||||
|
var createCommand = new CreateComment { Text = "text1" }; |
||||
|
var updateCommand = new UpdateComment { Text = "text2", CommentId = createCommand.CommentId }; |
||||
|
|
||||
|
await sut.ExecuteAsync(CreateCommentsCommand(createCommand)); |
||||
|
|
||||
|
var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand)); |
||||
|
|
||||
|
result.ShouldBeEquivalent(new EntitySavedResult(1)); |
||||
|
|
||||
|
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult |
||||
|
{ |
||||
|
CreatedComments = new List<Comment> |
||||
|
{ |
||||
|
new Comment(createCommand.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), createCommand.Actor, "text2") |
||||
|
}, |
||||
|
Version = 1 |
||||
|
}); |
||||
|
|
||||
|
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult |
||||
|
{ |
||||
|
UpdatedComments = new List<Comment> |
||||
|
{ |
||||
|
new Comment(createCommand.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), createCommand.Actor, "text2") |
||||
|
}, |
||||
|
Version = 1 |
||||
|
}); |
||||
|
|
||||
|
LastEvents |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateCommentsEvent(new CommentUpdated { CommentId = createCommand.CommentId, Text = updateCommand.Text }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Delete_should_create_events_and_update_state() |
||||
|
{ |
||||
|
var createCommand = new CreateComment { Text = "text1" }; |
||||
|
var updateCommand = new UpdateComment { Text = "text2", CommentId = createCommand.CommentId }; |
||||
|
var deleteCommand = new DeleteComment { CommentId = createCommand.CommentId }; |
||||
|
|
||||
|
await sut.ExecuteAsync(CreateCommentsCommand(createCommand)); |
||||
|
await sut.ExecuteAsync(CreateCommentsCommand(updateCommand)); |
||||
|
|
||||
|
var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand)); |
||||
|
|
||||
|
result.ShouldBeEquivalent(new EntitySavedResult(2)); |
||||
|
|
||||
|
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { Version = 2 }); |
||||
|
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult |
||||
|
{ |
||||
|
DeletedComments = new List<Guid> |
||||
|
{ |
||||
|
deleteCommand.CommentId |
||||
|
}, |
||||
|
Version = 2 |
||||
|
}); |
||||
|
sut.GetCommentsAsync(1).Result.Should().BeEquivalentTo(new CommentsResult |
||||
|
{ |
||||
|
DeletedComments = new List<Guid> |
||||
|
{ |
||||
|
deleteCommand.CommentId |
||||
|
}, |
||||
|
Version = 2 |
||||
|
}); |
||||
|
|
||||
|
LastEvents |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateCommentsEvent(new CommentDeleted { CommentId = createCommand.CommentId }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private Task ExecuteDeleteAsync() |
||||
|
{ |
||||
|
return sut.ExecuteAsync(CreateCommentsCommand(new DeleteComment())); |
||||
|
} |
||||
|
|
||||
|
protected T CreateCommentsEvent<T>(T @event) where T : CommentsEvent |
||||
|
{ |
||||
|
@event.CommentsId = commentsId; |
||||
|
|
||||
|
return CreateEvent(@event); |
||||
|
} |
||||
|
|
||||
|
protected T CreateCommentsCommand<T>(T command) where T : CommentsCommand |
||||
|
{ |
||||
|
command.CommentsId = commentsId; |
||||
|
|
||||
|
return CreateCommand(command); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,164 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Events.Comments; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments.Guards |
||||
|
{ |
||||
|
public class GuardCommentsTests |
||||
|
{ |
||||
|
private readonly RefToken user1 = new RefToken(RefTokenType.Subject, "1"); |
||||
|
private readonly RefToken user2 = new RefToken(RefTokenType.Subject, "2"); |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanCreate_should_throw_exception_if_text_not_defined() |
||||
|
{ |
||||
|
var command = new CreateComment(); |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardComments.CanCreate(command), |
||||
|
new ValidationError("Text is required.", "Text")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanCreate_should_not_throw_exception_if_text_defined() |
||||
|
{ |
||||
|
var command = new CreateComment { Text = "text" }; |
||||
|
|
||||
|
GuardComments.CanCreate(command); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdate_should_throw_exception_if_text_not_defined() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new UpdateComment { CommentId = commentId, Actor = user1 }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>> |
||||
|
{ |
||||
|
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() |
||||
|
}; |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardComments.CanUpdate(events, command), |
||||
|
new ValidationError("Text is required.", "Text")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdate_should_throw_exception_if_comment_from_another_user() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new UpdateComment { CommentId = commentId, Actor = user2, Text = "text2" }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>> |
||||
|
{ |
||||
|
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() |
||||
|
}; |
||||
|
|
||||
|
Assert.Throws<DomainException>(() => GuardComments.CanUpdate(events, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdate_should_throw_exception_if_comment_not_found() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new UpdateComment { CommentId = commentId, Actor = user1 }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>>(); |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(events, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdate_should_throw_exception_if_comment_deleted_found() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new UpdateComment { CommentId = commentId, Actor = user1 }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>> |
||||
|
{ |
||||
|
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(), |
||||
|
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>(), |
||||
|
}; |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(events, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdate_should_not_throw_exception_if_comment_from_same_user() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>> |
||||
|
{ |
||||
|
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() |
||||
|
}; |
||||
|
|
||||
|
GuardComments.CanUpdate(events, command); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanDelete_should_throw_exception_if_comment_from_another_user() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new DeleteComment { CommentId = commentId, Actor = user2 }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>> |
||||
|
{ |
||||
|
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() |
||||
|
}; |
||||
|
|
||||
|
Assert.Throws<DomainException>(() => GuardComments.CanDelete(events, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanDelete_should_throw_exception_if_comment_not_found() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new DeleteComment { CommentId = commentId, Actor = user1 }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>>(); |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(events, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanDelete_should_throw_exception_if_comment_deleted() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new DeleteComment { CommentId = commentId, Actor = user1 }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>> |
||||
|
{ |
||||
|
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(), |
||||
|
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>(), |
||||
|
}; |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(events, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanDelete_should_not_throw_exception_if_comment_from_same_user() |
||||
|
{ |
||||
|
var commentId = Guid.NewGuid(); |
||||
|
var command = new DeleteComment { CommentId = commentId, Actor = user1 }; |
||||
|
|
||||
|
var events = new List<Envelope<CommentsEvent>> |
||||
|
{ |
||||
|
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() |
||||
|
}; |
||||
|
|
||||
|
GuardComments.CanDelete(events, command); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue