Browse Source

Content handling

pull/1/head
Sebastian 9 years ago
parent
commit
e5aa5da577
  1. 2
      src/Squidex.Core/Schemas/Field.cs
  2. 23
      src/Squidex.Events/Contents/ContentCreated.cs
  3. 18
      src/Squidex.Events/Contents/ContentDeleted.cs
  4. 20
      src/Squidex.Events/Contents/ContentUpdated.cs
  5. 17
      src/Squidex.Events/EventExtensions.cs
  6. 6
      src/Squidex.Infrastructure/Extensions.cs
  7. 12
      src/Squidex.Infrastructure/Guard.cs
  8. 5
      src/Squidex.Read/Apps/Repositories/IAppRepository.cs
  9. 3
      src/Squidex.Read/Apps/Services/IAppProvider.cs
  10. 58
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  11. 2
      src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs
  12. 4
      src/Squidex.Read/Schemas/Services/ISchemaProvider.cs
  13. 4
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  14. 11
      src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs
  15. 2
      src/Squidex.Write/Apps/AppCommandHandler.cs
  16. 14
      src/Squidex.Write/Contents/Commands/CreateContent.cs
  17. 14
      src/Squidex.Write/Contents/Commands/DeleteContent.cs
  18. 14
      src/Squidex.Write/Contents/Commands/UpdateContent.cs
  19. 89
      src/Squidex.Write/Contents/ContentCommandHandler.cs
  20. 27
      src/Squidex.Write/Contents/ContentDataCommand.cs
  21. 99
      src/Squidex.Write/Contents/ContentDomainObject.cs
  22. 17
      src/Squidex.Write/ISchemaCommand.cs
  23. 27
      src/Squidex.Write/SchemaAggregateCommand.cs
  24. 12
      src/Squidex.Write/SchemaCommand.cs
  25. 6
      src/Squidex.Write/Schemas/Commands/AddField.cs
  26. 3
      src/Squidex.Write/Schemas/Commands/DeleteField.cs
  27. 3
      src/Squidex.Write/Schemas/Commands/DeleteSchema.cs
  28. 3
      src/Squidex.Write/Schemas/Commands/DisableField.cs
  29. 3
      src/Squidex.Write/Schemas/Commands/EnableField.cs
  30. 3
      src/Squidex.Write/Schemas/Commands/HideField.cs
  31. 2
      src/Squidex.Write/Schemas/Commands/PublishSchema.cs
  32. 3
      src/Squidex.Write/Schemas/Commands/ShowField.cs
  33. 2
      src/Squidex.Write/Schemas/Commands/UnpublishSchema.cs
  34. 2
      src/Squidex.Write/Schemas/Commands/UpdateField.cs
  35. 2
      src/Squidex.Write/Schemas/Commands/UpdateSchema.cs
  36. 2
      src/Squidex.Write/Schemas/SchemaCommandHandler.cs
  37. 6
      src/Squidex/Config/Domain/WriteModule.cs
  38. 69
      src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaAggregateIdHandler.cs
  39. 59
      src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs
  40. 30
      tests/Squidex.Infrastructure.Tests/GuardTests.cs
  41. 4
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  42. 130
      tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs
  43. 147
      tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs
  44. 4
      tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs
  45. 0
      tests/Squidex.Write.Tests/x.ClearAsync()
  46. 0
      tests/Squidex.Write.Tests/x.GetEventsAsync()

2
src/Squidex.Core/Schemas/Field.cs

@ -48,7 +48,7 @@ namespace Squidex.Core.Schemas
protected Field(long id, string name)
{
Guard.ValidSlug(name, nameof(name));
Guard.ValidPropertyName(name, nameof(name));
Guard.GreaterThan(id, 0, nameof(id));
this.id = id;

23
src/Squidex.Events/Contents/ContentCreated.cs

@ -0,0 +1,23 @@
// ==========================================================================
// ContentCreated.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Contents
{
[TypeName("ContentCreatedEvent")]
public class ContentCreated : IEvent
{
public Guid SchemaId { get; set; }
public JObject Data { get; set; }
}
}

18
src/Squidex.Events/Contents/ContentDeleted.cs

@ -0,0 +1,18 @@
// ==========================================================================
// ContentDeleted.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Contents
{
[TypeName("ContentDeletedEvent")]
public class ContentDeleted : IEvent
{
}
}

20
src/Squidex.Events/Contents/ContentUpdated.cs

@ -0,0 +1,20 @@
// ==========================================================================
// ContentUpdated.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Contents
{
[TypeName("ContentUpdatedEvent")]
public class ContentUpdated : IEvent
{
public JObject Data { get; set; }
}
}

17
src/Squidex.Events/EventExtensions.cs

@ -30,5 +30,22 @@ namespace Squidex.Events
return envelope;
}
public static bool HasSchemaId(this EnvelopeHeaders headers)
{
return headers.Contains("SchemaId");
}
public static Guid SchemaId(this EnvelopeHeaders headers)
{
return headers["SchemaId"].ToGuid(CultureInfo.InvariantCulture);
}
public static Envelope<T> SetSchemaId<T>(this Envelope<T> envelope, Guid value) where T : class
{
envelope.Headers.Set("SchemaId", value);
return envelope;
}
}
}

6
src/Squidex.Infrastructure/Extensions.cs

@ -15,12 +15,18 @@ namespace Squidex.Infrastructure
public static class Extensions
{
private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled);
private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled);
public static bool IsSlug(this string value)
{
return value != null && SlugRegex.IsMatch(value);
}
public static bool IsPropertyName(this string value)
{
return value != null && PropertyNameRegex.IsMatch(value);
}
public static bool IsBetween<TValue>(this TValue value, TValue low, TValue high) where TValue : IComparable
{
return Comparer<TValue>.Default.Compare(low, value) <= 0 && Comparer<TValue>.Default.Compare(high, value) >= 0;

12
src/Squidex.Infrastructure/Guard.cs

@ -51,6 +51,18 @@ namespace Squidex.Infrastructure
}
}
[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidPropertyName(string target, string parameterName)
{
NotNullOrEmpty(target, parameterName);
if (!target.IsPropertyName())
{
throw new ArgumentException("Target is not a valid property name.", parameterName);
}
}
[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void HasType<T>(object target, string parameterName)

5
src/Squidex.Read/Apps/Repositories/IAppRepository.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -15,6 +16,8 @@ namespace Squidex.Read.Apps.Repositories
{
Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string subjectId);
Task<IAppEntity> FindAppByNameAsync(string name);
Task<IAppEntity> FindAppAsync(Guid appId);
Task<IAppEntity> FindAppAsync(string name);
}
}

3
src/Squidex.Read/Apps/Services/IAppProvider.cs

@ -6,12 +6,15 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Read.Apps.Services
{
public interface IAppProvider
{
Task<IAppEntity> FindAppByIdAsync(Guid id);
Task<IAppEntity> FindAppByNameAsync(string name);
}
}

58
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -23,39 +23,63 @@ namespace Squidex.Read.Apps.Services.Implementations
public class CachingAppProvider : CachingProvider, IAppProvider, ICatchEventConsumer
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
private readonly IAppRepository appRepository;
private readonly IAppRepository repository;
private sealed class CacheItem
{
public IAppEntity Entity;
public string Name;
}
public CachingAppProvider(IMemoryCache cache, IAppRepository appRepository)
public CachingAppProvider(IMemoryCache cache, IAppRepository repository)
: base(cache)
{
Guard.NotNull(cache, nameof(cache));
this.appRepository = appRepository;
this.repository = repository;
}
public async Task<IAppEntity> FindAppByIdAsync(Guid appId)
{
var cacheKey = BuildIdCacheKey(appId);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem == null)
{
var entity = await repository.FindAppAsync(appId);
cacheItem = new CacheItem { Entity = entity, Name = entity?.Name };
Cache.Set(cacheKey, cacheItem, CacheDuration);
if (cacheItem.Entity != null)
{
Cache.Set(BuildIdCacheKey(cacheItem.Entity.Id), cacheItem, CacheDuration);
}
}
return cacheItem.Entity;
}
public async Task<IAppEntity> FindAppByNameAsync(string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
var cacheKey = BuildModelCacheKey(name);
var cacheKey = BuildNameCacheKey(name);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem == null)
{
var app = await appRepository.FindAppByNameAsync(name);
var entity = await repository.FindAppAsync(name);
cacheItem = new CacheItem { Entity = app };
cacheItem = new CacheItem { Entity = entity, Name = name };
Cache.Set(cacheKey, cacheItem, new MemoryCacheEntryOptions { SlidingExpiration = CacheDuration });
Cache.Set(cacheKey, cacheItem, CacheDuration);
if (cacheItem.Entity != null)
{
Cache.Set(BuildNamesCacheKey(cacheItem.Entity.Id), cacheItem.Entity.Name, CacheDuration);
Cache.Set(BuildIdCacheKey(cacheItem.Entity.Id), cacheItem, CacheDuration);
}
}
@ -73,25 +97,29 @@ namespace Squidex.Read.Apps.Services.Implementations
@event.Payload is AppLanguageRemoved ||
@event.Payload is AppMasterLanguageSet)
{
var appName = Cache.Get<string>(BuildNamesCacheKey(@event.Headers.AggregateId()));
var cacheKey = BuildIdCacheKey(@event.Headers.AggregateId());
if (appName != null)
var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem?.Name != null)
{
Cache.Remove(BuildModelCacheKey(appName));
Cache.Remove(BuildNameCacheKey(cacheItem.Name));
}
Cache.Remove(cacheKey);
}
return Task.FromResult(true);
}
private static string BuildNamesCacheKey(Guid schemaId)
private static string BuildNameCacheKey(string name)
{
return $"App_Names_{schemaId}";
return $"App_Ids_{name}";
}
private static string BuildModelCacheKey(string name)
private static string BuildIdCacheKey(Guid schemaId)
{
return $"App_{name}";
return $"App_Names_{schemaId}";
}
}
}

2
src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs

@ -104,7 +104,7 @@ namespace Squidex.Read.Schemas
private async Task<string> FindSchemaNameAsync(EnvelopeHeaders headers)
{
var schema = await schemaProvider.ProviderSchemaByIdAsync(headers.AggregateId());
var schema = await schemaProvider.FindSchemaByIdAsync(headers.AggregateId());
return schema.Label ?? schema.Name;
}

4
src/Squidex.Read/Schemas/Services/ISchemaProvider.cs

@ -14,8 +14,8 @@ namespace Squidex.Read.Schemas.Services
{
public interface ISchemaProvider
{
Task<ISchemaEntityWithSchema> ProviderSchemaByIdAsync(Guid schemaId);
Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid schemaId);
Task<ISchemaEntityWithSchema> ProvideSchemaByNameAsync(Guid appId, string name);
Task<ISchemaEntityWithSchema> FindSchemaByNameAsync(Guid appId, string name);
}
}

4
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -41,7 +41,7 @@ namespace Squidex.Read.Schemas.Services.Implementations
this.repository = repository;
}
public async Task<ISchemaEntityWithSchema> ProviderSchemaByIdAsync(Guid schemaId)
public async Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid schemaId)
{
var cacheKey = BuildIdCacheKey(schemaId);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
@ -63,7 +63,7 @@ namespace Squidex.Read.Schemas.Services.Implementations
return cacheItem.Entity;
}
public async Task<ISchemaEntityWithSchema> ProvideSchemaByNameAsync(Guid appId, string name)
public async Task<ISchemaEntityWithSchema> FindSchemaByNameAsync(Guid appId, string name)
{
Guard.NotNullOrEmpty(name, nameof(name));

11
src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Driver;
@ -53,7 +54,15 @@ namespace Squidex.Store.MongoDb.Apps
return entities;
}
public async Task<IAppEntity> FindAppByNameAsync(string name)
public async Task<IAppEntity> FindAppAsync(Guid id)
{
var entity =
await Collection.Find(s => s.Id == id).FirstOrDefaultAsync();
return entity;
}
public async Task<IAppEntity> FindAppAsync(string name)
{
var entity =
await Collection.Find(s => s.Name == name).FirstOrDefaultAsync();

2
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -42,7 +42,7 @@ namespace Squidex.Write.Apps
protected async Task On(CreateApp command, CommandContext context)
{
if (await appRepository.FindAppByNameAsync(command.Name) != null)
if (await appRepository.FindAppAsync(command.Name) != null)
{
var error =
new ValidationError($"An app with name '{command.Name}' already exists",

14
src/Squidex.Write/Contents/Commands/CreateContent.cs

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

14
src/Squidex.Write/Contents/Commands/DeleteContent.cs

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

14
src/Squidex.Write/Contents/Commands/UpdateContent.cs

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

89
src/Squidex.Write/Contents/ContentCommandHandler.cs

@ -0,0 +1,89 @@
// ==========================================================================
// ContentCommandHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching;
using Squidex.Read.Apps.Services;
using Squidex.Read.Schemas.Services;
using Squidex.Write.Contents.Commands;
namespace Squidex.Write.Contents
{
public class ContentCommandHandler : ICommandHandler
{
private readonly IAggregateHandler handler;
private readonly IAppProvider appProvider;
private readonly ISchemaProvider schemaProvider;
public ContentCommandHandler(
IAggregateHandler handler,
IAppProvider appProvider,
ISchemaProvider schemaProvider)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(schemaProvider, nameof(schemaProvider));
this.handler = handler;
this.appProvider = appProvider;
this.schemaProvider = schemaProvider;
}
protected async Task On(CreateContent command, CommandContext context)
{
await ValidateAsync(command, () => "Failed to create content");
await handler.CreateAsync<ContentDomainObject>(command, s =>
{
s.Create(command);
context.Succeed(command.AggregateId);
});
}
protected async Task On(UpdateContent command, CommandContext context)
{
await ValidateAsync(command, () => "Failed to update content");
await handler.UpdateAsync<ContentDomainObject>(command, s => s.Update(command));
}
protected Task On(DeleteContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Delete(command));
}
public Task<bool> HandleAsync(CommandContext context)
{
return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context);
}
private async Task ValidateAsync(ContentDataCommand command, Func<string> message)
{
Guard.Valid(command, nameof(command), message);
var taskForApp = appProvider.FindAppByIdAsync(command.AppId);
var taskForSchema = schemaProvider.FindSchemaByIdAsync(command.SchemaId);
await Task.WhenAll(taskForApp, taskForSchema);
var errors = new List<ValidationError>();
await taskForSchema.Result.Schema.ValidateAsync(command.Data, errors, new HashSet<Language>(taskForApp.Result.Languages));
if (errors.Count > 0)
{
throw new ValidationException(message(), errors);
}
}
}
}

27
src/Squidex.Write/Contents/ContentDataCommand.cs

@ -0,0 +1,27 @@
// ==========================================================================
// ContentDataCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
namespace Squidex.Write.Contents
{
public class ContentDataCommand : SchemaCommand, IValidatable
{
public JObject Data { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (Data == null)
{
errors.Add(new ValidationError("Data cannot be null", nameof(Data)));
}
}
}
}

99
src/Squidex.Write/Contents/ContentDomainObject.cs

@ -0,0 +1,99 @@
// ==========================================================================
// ContentDomainObject.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
using Squidex.Write.Contents.Commands;
namespace Squidex.Write.Contents
{
public class ContentDomainObject : DomainObject
{
private bool isDeleted;
private bool isCreated;
public bool IsDeleted
{
get { return isDeleted; }
}
public ContentDomainObject(Guid id, int version)
: base(id, version)
{
}
protected void On(ContentCreated @event)
{
isCreated = true;
}
protected void On(ContentDeleted @event)
{
isDeleted = true;
}
public ContentDomainObject Create(CreateContent command)
{
Guard.Valid(command, nameof(command), () => "Cannot create content");
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new ContentCreated()));
return this;
}
public ContentDomainObject Update(UpdateContent command)
{
Guard.Valid(command, nameof(command), () => "Cannot update content");
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated()));
return this;
}
public ContentDomainObject Delete(DeleteContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentDeleted()));
return this;
}
private void VerifyNotCreated()
{
if (isCreated)
{
throw new DomainException("Content has already been created.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (isDeleted || !isCreated)
{
throw new DomainException("Content has already been deleted or not created yet.");
}
}
protected override void DispatchEvent(Envelope<IEvent> @event)
{
this.DispatchAction(@event.Payload);
}
}
}

17
src/Squidex.Write/ISchemaCommand.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ISchemaCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Write
{
public interface ISchemaCommand : IAppCommand
{
Guid SchemaId { get; set; }
}
}

27
src/Squidex.Write/SchemaAggregateCommand.cs

@ -0,0 +1,27 @@
// ==========================================================================
// SchemaAggregateCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Write
{
public abstract class SchemaAggregateCommand : AppCommand, ISchemaCommand
{
Guid ISchemaCommand.SchemaId
{
get
{
return AggregateId;
}
set
{
AggregateId = value;
}
}
}
}

12
src/Squidex.Write/SchemaCommand.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Squidex.Write
{
public abstract class SchemaCommand : AppCommand, ISchemaCommand
{
public Guid SchemaId { get; set; }
}
}

6
src/Squidex.Write/Schemas/Commands/AddField.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Write.Schemas.Commands
{
public class AddField : AppCommand, IValidatable
public class AddField : SchemaAggregateCommand, IValidatable
{
public string Name { get; set; }
@ -20,9 +20,9 @@ namespace Squidex.Write.Schemas.Commands
public void Validate(IList<ValidationError> errors)
{
if (!Name.IsSlug())
if (!Name.IsPropertyName())
{
errors.Add(new ValidationError("DisplayName must be a valid slug", nameof(Name)));
errors.Add(new ValidationError("DisplayName must be a valid property name", nameof(Name)));
}
if (Properties == null)

3
src/Squidex.Write/Schemas/Commands/DeleteField.cs

@ -5,9 +5,10 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Write.Schemas.Commands
{
public class DeleteField : AppCommand
public class DeleteField : SchemaAggregateCommand
{
public long FieldId { get; set; }
}

3
src/Squidex.Write/Schemas/Commands/DeleteSchema.cs

@ -5,9 +5,10 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Write.Schemas.Commands
{
public class DeleteSchema : AppCommand
public class DeleteSchema : SchemaAggregateCommand
{
}
}

3
src/Squidex.Write/Schemas/Commands/DisableField.cs

@ -5,9 +5,10 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Write.Schemas.Commands
{
public class DisableField : AppCommand
public class DisableField : SchemaAggregateCommand
{
public long FieldId { get; set; }
}

3
src/Squidex.Write/Schemas/Commands/EnableField.cs

@ -5,9 +5,10 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Write.Schemas.Commands
{
public class EnableField : AppCommand
public class EnableField : SchemaAggregateCommand
{
public long FieldId { get; set; }
}

3
src/Squidex.Write/Schemas/Commands/HideField.cs

@ -5,9 +5,10 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Write.Schemas.Commands
{
public class HideField : AppCommand
public class HideField : SchemaAggregateCommand
{
public long FieldId { get; set; }
}

2
src/Squidex.Write/Schemas/Commands/PublishSchema.cs

@ -8,7 +8,7 @@
namespace Squidex.Write.Schemas.Commands
{
public class PublishSchema : AppCommand
public class PublishSchema : SchemaAggregateCommand
{
}
}

3
src/Squidex.Write/Schemas/Commands/ShowField.cs

@ -5,9 +5,10 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Write.Schemas.Commands
{
public class ShowField : AppCommand
public class ShowField : SchemaAggregateCommand
{
public long FieldId { get; set; }
}

2
src/Squidex.Write/Schemas/Commands/UnpublishSchema.cs

@ -8,7 +8,7 @@
namespace Squidex.Write.Schemas.Commands
{
public class UnpublishSchema : AppCommand
public class UnpublishSchema : SchemaAggregateCommand
{
}
}

2
src/Squidex.Write/Schemas/Commands/UpdateField.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Write.Schemas.Commands
{
public class UpdateField : AppCommand, IValidatable
public class UpdateField : SchemaAggregateCommand, IValidatable
{
public long FieldId { get; set; }

2
src/Squidex.Write/Schemas/Commands/UpdateSchema.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Write.Schemas.Commands
{
public class UpdateSchema : AppCommand, IValidatable
public class UpdateSchema : SchemaAggregateCommand, IValidatable
{
public SchemaProperties Properties { get; set; }

2
src/Squidex.Write/Schemas/SchemaCommandHandler.cs

@ -32,7 +32,7 @@ namespace Squidex.Write.Schemas
protected async Task On(CreateSchema command, CommandContext context)
{
if (await schemas.ProvideSchemaByNameAsync(command.AppId, command.Name) != null)
if (await schemas.FindSchemaByNameAsync(command.AppId, command.Name) != null)
{
var error =
new ValidationError($"A schema with name '{command.Name}' already exists", "DisplayName",

6
src/Squidex/Config/Domain/WriteModule.cs

@ -19,7 +19,7 @@ namespace Squidex.Config.Domain
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<EnrichWithAppIdHandler>()
builder.RegisterType<EnrichWithTimestampHandler>()
.As<ICommandHandler>()
.SingleInstance();
@ -27,11 +27,11 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<EnrichWithTimestampHandler>()
builder.RegisterType<EnrichWithAppIdHandler>()
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<EnrichWithSchemaAggregateIdHandler>()
builder.RegisterType<EnrichWithSchemaIdHandler>()
.As<ICommandHandler>()
.SingleInstance();

69
src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaAggregateIdHandler.cs

@ -1,69 +0,0 @@
// ==========================================================================
// EnrichWithSchemaAggregateIdHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Read.Schemas.Services;
using Squidex.Write;
using Squidex.Write.Schemas;
// ReSharper disable InvertIf
namespace Squidex.Pipeline.CommandHandlers
{
public sealed class EnrichWithSchemaAggregateIdHandler : ICommandHandler
{
private readonly ISchemaProvider schemaProvider;
private readonly IActionContextAccessor actionContextAccessor;
public EnrichWithSchemaAggregateIdHandler(ISchemaProvider schemaProvider, IActionContextAccessor actionContextAccessor)
{
this.schemaProvider = schemaProvider;
this.actionContextAccessor = actionContextAccessor;
}
public async Task<bool> HandleAsync(CommandContext context)
{
var aggregateCommand = context.Command as IAggregateCommand;
if (aggregateCommand == null || aggregateCommand.AggregateId != Guid.Empty)
{
return false;
}
var appCommand = context.Command as IAppCommand;
if (appCommand == null)
{
return false;
}
var routeValues = actionContextAccessor.ActionContext.RouteData.Values;
if (routeValues.ContainsKey("name"))
{
var schemaName = routeValues["name"].ToString();
var schema = await schemaProvider.ProvideSchemaByNameAsync(appCommand.AppId, schemaName);
if (schema == null)
{
throw new DomainObjectNotFoundException(schemaName, typeof(SchemaDomainObject));
}
aggregateCommand.AggregateId = schema.Id;
}
return false;
}
}
}

59
src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs

@ -0,0 +1,59 @@
// ==========================================================================
// EnrichWithSchemaIdHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Read.Schemas.Services;
using Squidex.Write;
using Squidex.Write.Schemas;
// ReSharper disable InvertIf
namespace Squidex.Pipeline.CommandHandlers
{
public sealed class EnrichWithSchemaIdHandler : ICommandHandler
{
private readonly ISchemaProvider schemaProvider;
private readonly IActionContextAccessor actionContextAccessor;
public EnrichWithSchemaIdHandler(ISchemaProvider schemaProvider, IActionContextAccessor actionContextAccessor)
{
this.schemaProvider = schemaProvider;
this.actionContextAccessor = actionContextAccessor;
}
public async Task<bool> HandleAsync(CommandContext context)
{
var schemaCommand = context.Command as ISchemaCommand;
if (schemaCommand != null)
{
var routeValues = actionContextAccessor.ActionContext.RouteData.Values;
if (routeValues.ContainsKey("name"))
{
var schemaName = routeValues["name"].ToString();
var schema = await schemaProvider.FindSchemaByNameAsync(schemaCommand.AppId, schemaName);
if (schema == null)
{
throw new DomainObjectNotFoundException(schemaName, typeof(SchemaDomainObject));
}
schemaCommand.SchemaId = schema.Id;
}
}
return false;
}
}
}

30
tests/Squidex.Infrastructure.Tests/GuardTests.cs

@ -150,9 +150,10 @@ namespace Squidex.Infrastructure
[InlineData(" not-a-slug ")]
[InlineData("-not-a-slug-")]
[InlineData("not$-a-slug")]
[InlineData("not-a-Slug")]
public void ValidSlug_should_throw_for_invalid_slugs(string slug)
{
Assert.Throws<ArgumentException>(() => Guard.ValidSlug(slug, "slug"));
Assert.Throws<ArgumentException>(() => Guard.ValidSlug(slug, "parameter"));
}
[Theory]
@ -165,6 +166,33 @@ namespace Squidex.Infrastructure
Guard.ValidSlug(slug, "parameter");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(" Not a Property ")]
[InlineData(" not--a--property ")]
[InlineData(" not-a-property ")]
[InlineData("-not-a-property-")]
[InlineData("not$-a-property")]
public void ValidPropertyName_should_throw_for_invalid_slugs(string slug)
{
Assert.Throws<ArgumentException>(() => Guard.ValidPropertyName(slug, "property"));
}
[Theory]
[InlineData("property")]
[InlineData("property23")]
[InlineData("other-property")]
[InlineData("other-Property")]
[InlineData("otherProperty")]
[InlineData("just-another-property")]
[InlineData("just-Another-Property")]
[InlineData("justAnotherProperty")]
public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property)
{
Guard.ValidPropertyName(property, "parameter");
}
[Theory]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]

4
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -53,7 +53,7 @@ namespace Squidex.Write.Apps
var command = new CreateApp { Name = appName, AggregateId = Id, Actor = subjectId };
var context = new CommandContext(command);
appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult(new Mock<IAppEntity>().Object)).Verifiable();
appRepository.Setup(x => x.FindAppAsync(appName)).Returns(Task.FromResult(new Mock<IAppEntity>().Object)).Verifiable();
await TestCreate(app, async _ =>
{
@ -69,7 +69,7 @@ namespace Squidex.Write.Apps
var command = new CreateApp { Name = appName, AggregateId = Id, Actor = subjectId };
var context = new CommandContext(command);
appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult<IAppEntity>(null)).Verifiable();
appRepository.Setup(x => x.FindAppAsync(appName)).Returns(Task.FromResult<IAppEntity>(null)).Verifiable();
await TestCreate(app, async _ =>
{

130
tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

@ -0,0 +1,130 @@
// ==========================================================================
// ContentCommandHandlerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json.Linq;
using Squidex.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Read.Apps;
using Squidex.Read.Apps.Services;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas.Services;
using Squidex.Write.Contents.Commands;
using Squidex.Write.Utils;
using Xunit;
// ReSharper disable ConvertToConstant.Local
namespace Squidex.Write.Contents
{
public class ContentCommandHandlerTests : HandlerTestBase<ContentDomainObject>
{
private readonly ContentCommandHandler sut;
private readonly ContentDomainObject content;
private readonly Mock<ISchemaProvider> schemaProvider = new Mock<ISchemaProvider>();
private readonly Mock<IAppProvider> appProvider = new Mock<IAppProvider>();
private readonly Mock<ISchemaEntityWithSchema> schemaEntity = new Mock<ISchemaEntityWithSchema>();
private readonly Mock<IAppEntity> appEntity = new Mock<IAppEntity>();
private readonly Guid schemaId = Guid.NewGuid();
private readonly Guid appId = Guid.NewGuid();
private readonly JObject data = new JObject(new JProperty("field", 1));
public ContentCommandHandlerTests()
{
var schema =
Schema.Create("my-schema", new SchemaProperties())
.AddOrUpdateField(new NumberField(1, "field", new NumberFieldProperties { IsRequired = true }));
content = new ContentDomainObject(Id, 0);
sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object);
appEntity.Setup(x => x.Languages).Returns(new[] { Language.GetLanguage("de") });
appProvider.Setup(x => x.FindAppByIdAsync(appId)).Returns(Task.FromResult(appEntity.Object));
schemaEntity.Setup(x => x.Schema).Returns(schema);
schemaProvider.Setup(x => x.FindSchemaByIdAsync(schemaId)).Returns(Task.FromResult(schemaEntity.Object));
}
[Fact]
public async Task Create_should_throw_exception_if_data_is_not_valid()
{
var command = new CreateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = new JObject() };
var context = new CommandContext(command);
await TestCreate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Create_should_create_content()
{
var command = new CreateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = data };
var context = new CommandContext(command);
await TestCreate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(Id, context.Result<Guid>());
}
[Fact]
public async Task Update_should_throw_exception_if_data_is_not_valid()
{
CreateContent();
var command = new UpdateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = new JObject() };
var context = new CommandContext(command);
await TestUpdate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Update_should_update_domain_object()
{
CreateContent();
var command = new UpdateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = data };
var context = new CommandContext(command);
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Delete_should_update_domain_object()
{
CreateContent();
var command = new DeleteContent { AggregateId = Id };
var context = new CommandContext(command);
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
}
private void CreateContent()
{
content.Create(new CreateContent { Data = data });
}
}
}

147
tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs

@ -0,0 +1,147 @@
// ==========================================================================
// ContentDomainObjectTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using Squidex.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Write.Contents.Commands;
using Xunit;
// ReSharper disable ConvertToConstant.Local
namespace Squidex.Write.Contents
{
[Collection("Content")]
public class ContentDomainObjectTests
{
private readonly Guid appId = Guid.NewGuid();
private readonly ContentDomainObject sut;
private readonly JObject data = new JObject();
public ContentDomainObjectTests()
{
sut = new ContentDomainObject(Guid.NewGuid(), 0);
}
[Fact]
public void Create_should_throw_if_created()
{
sut.Create(new CreateContent { Data = data });
Assert.Throws<DomainException>(() => sut.Create(new CreateContent { Data = data }));
}
[Fact]
public void Create_should_throw_if_command_is_not_valid()
{
Assert.Throws<ValidationException>(() => sut.Create(new CreateContent()));
}
[Fact]
public void Create_should_create_events()
{
sut.Create(new CreateContent { Data = data, AppId = appId });
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new ContentCreated { Data = data }
});
}
[Fact]
public void Update_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.Update(new UpdateContent { Data = data }));
}
[Fact]
public void Update_should_throw_if_schema_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<ValidationException>(() => sut.Update(new UpdateContent()));
}
[Fact]
public void Update_should_throw_if_command_is_not_valid()
{
CreateContent();
Assert.Throws<ValidationException>(() => sut.Update(new UpdateContent()));
}
[Fact]
public void Update_should_create_events()
{
CreateContent();
sut.Update(new UpdateContent { Data = data });
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new ContentUpdated { Data = data }
});
}
[Fact]
public void Delete_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.Delete(new DeleteContent()));
}
[Fact]
public void Delete_should_throw_if_already_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() => sut.Delete(new DeleteContent()));
}
[Fact]
public void Delete_should_update_properties_create_events()
{
CreateContent();
sut.Delete(new DeleteContent());
Assert.True(sut.IsDeleted);
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new ContentDeleted()
});
}
private void CreateContent()
{
sut.Create(new CreateContent { Data = data, AppId = appId });
((IAggregate)sut).ClearUncommittedEvents();
}
private void DeleteContent()
{
sut.Delete(new DeleteContent());
((IAggregate)sut).ClearUncommittedEvents();
}
}
}

4
tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

@ -51,7 +51,7 @@ namespace Squidex.Write.Schemas
var command = new CreateSchema { Name = schemaName, AppId = appId, AggregateId = Id };
var context = new CommandContext(command);
schemaProvider.Setup(x => x.ProvideSchemaByNameAsync(appId, schemaName)).Returns(Task.FromResult(new Mock<ISchemaEntityWithSchema>().Object)).Verifiable();
schemaProvider.Setup(x => x.FindSchemaByNameAsync(appId, schemaName)).Returns(Task.FromResult(new Mock<ISchemaEntityWithSchema>().Object)).Verifiable();
await TestCreate(schema, async _ =>
{
@ -67,7 +67,7 @@ namespace Squidex.Write.Schemas
var command = new CreateSchema { Name = schemaName, AppId = appId, AggregateId = Id };
var context = new CommandContext(command);
schemaProvider.Setup(x => x.ProvideSchemaByNameAsync(Id, schemaName)).Returns(Task.FromResult<ISchemaEntityWithSchema>(null)).Verifiable();
schemaProvider.Setup(x => x.FindSchemaByNameAsync(Id, schemaName)).Returns(Task.FromResult<ISchemaEntityWithSchema>(null)).Verifiable();
await TestCreate(schema, async _ =>
{

0
tests/Squidex.Write.Tests/x.ClearAsync()

0
tests/Squidex.Write.Tests/x.GetEventsAsync()

Loading…
Cancel
Save