Browse Source

Merge pull request #208 from Squidex/feature-app-pattern

Feature app pattern
pull/214/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
a0927f9b17
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 51
      src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
  2. 57
      src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  3. 37
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs
  4. 39
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs
  5. 10
      src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs
  6. 30
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  7. 64
      src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  8. 28
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs
  9. 4
      src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
  10. 17
      src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs
  11. 23
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs
  12. 97
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs
  13. 2
      src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
  14. 26
      src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs
  15. 2
      src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs
  16. 18
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  17. 25
      src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs
  18. 21
      src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs
  19. 25
      src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs
  20. 2
      src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs
  21. 130
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  22. 38
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs
  23. 11
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs
  24. 7
      src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  25. 9
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  26. 3
      src/Squidex/Config/Domain/SerializationServices.cs
  27. 26
      src/Squidex/Config/Domain/WriteServices.cs
  28. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  29. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  30. 1
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  31. 12
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  32. 7
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html
  33. 52
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  34. 2
      src/Squidex/app/features/settings/declarations.ts
  35. 24
      src/Squidex/app/features/settings/module.ts
  36. 2
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  37. 45
      src/Squidex/app/features/settings/pages/patterns/pattern.component.html
  38. 13
      src/Squidex/app/features/settings/pages/patterns/pattern.component.scss
  39. 99
      src/Squidex/app/features/settings/pages/patterns/pattern.component.ts
  40. 41
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html
  41. 2
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss
  42. 87
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts
  43. 6
      src/Squidex/app/features/settings/settings-area.component.html
  44. 1
      src/Squidex/app/shared/declarations-base.ts
  45. 2
      src/Squidex/app/shared/module.ts
  46. 2
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  47. 2
      src/Squidex/app/shared/services/app-languages.service.ts
  48. 174
      src/Squidex/app/shared/services/app-patterns.service.spec.ts
  49. 130
      src/Squidex/app/shared/services/app-patterns.service.ts
  50. 3
      src/Squidex/app/shared/services/ui.service.spec.ts
  51. 5
      src/Squidex/app/shared/services/ui.service.ts
  52. 2
      src/Squidex/appsettings.json
  53. 44
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs
  54. 77
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs
  55. 1
      tests/Squidex.Domain.Apps.Core.Tests/TestData.cs
  56. 44
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs
  57. 101
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  58. 169
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs
  59. 43
      tools/Migrate_01/Migration01_FromCqrs.cs
  60. 71
      tools/Migrate_01/Migration02_AddPatterns.cs

51
src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs

@ -0,0 +1,51 @@
// ==========================================================================
// AppPattern.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPattern
{
private readonly string name;
private readonly string pattern;
private readonly string message;
public string Name
{
get { return name; }
}
public string Pattern
{
get { return pattern; }
}
public string Message
{
get { return message; }
}
public AppPattern(string name, string pattern, string message = null)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(pattern, nameof(pattern));
this.name = name;
this.pattern = pattern;
this.message = message;
}
[Pure]
public AppPattern Update(string name, string pattern, string message)
{
return new AppPattern(name, pattern, message);
}
}
}

57
src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -0,0 +1,57 @@
// ==========================================================================
// AppPatterns.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPatterns : DictionaryWrapper<Guid, AppPattern>
{
public static readonly AppPatterns Empty = new AppPatterns();
private AppPatterns()
: base(ImmutableDictionary<Guid, AppPattern>.Empty)
{
}
public AppPatterns(ImmutableDictionary<Guid, AppPattern> inner)
: base(inner)
{
}
[Pure]
public AppPatterns Add(Guid id, string name, string pattern, string message)
{
var newPattern = new AppPattern(name, pattern, message);
return new AppPatterns(Inner.Add(id, newPattern));
}
[Pure]
public AppPatterns Remove(Guid id)
{
return new AppPatterns(Inner.Remove(id));
}
[Pure]
public AppPatterns Update(Guid id, string name, string pattern, string message)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(pattern, nameof(pattern));
if (!TryGetValue(id, out var appPattern))
{
return this;
}
return new AppPatterns(Inner.SetItem(id, appPattern.Update(name, pattern, message)));
}
}
}

37
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs

@ -0,0 +1,37 @@
// ==========================================================================
// AppPatternsConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class AppPatternsConverter : JsonClassConverter<AppPatterns>
{
protected override void WriteValue(JsonWriter writer, AppPatterns value, JsonSerializer serializer)
{
var json = new Dictionary<Guid, JsonAppPattern>(value.Count);
foreach (var client in value)
{
json.Add(client.Key, new JsonAppPattern(client.Value));
}
serializer.Serialize(writer, json);
}
protected override AppPatterns ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<Guid, JsonAppPattern>>(reader);
return new AppPatterns(json.ToImmutableDictionary(x => x.Key, x => x.Value.ToPattern()));
}
}
}

39
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs

@ -0,0 +1,39 @@
// ==========================================================================
// JsonAppPattern.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Newtonsoft.Json;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public class JsonAppPattern
{
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public string Pattern { get; set; }
[JsonProperty]
public string Message { get; set; }
public JsonAppPattern()
{
}
public JsonAppPattern(AppPattern pattern)
{
SimpleMapper.Map(pattern, this);
}
public AppPattern ToPattern()
{
return new AppPattern(Name, Pattern, Message);
}
}
}

10
src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Apps.State;
@ -46,6 +47,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
return appEntity != null ? Guid.Parse(appEntity["_id"].AsString) : Guid.Empty;
}
public async Task<IReadOnlyList<Guid>> QueryAppIdsAsync()
{
var appEntities =
await Collection.Find(new BsonDocument()).Only(x => x.Id)
.ToListAsync();
return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
}
public async Task<IReadOnlyList<Guid>> QueryUserAppIdsAsync(string userId)
{
var appEntities =

30
src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs

@ -138,6 +138,36 @@ namespace Squidex.Domain.Apps.Entities.Apps
});
}
protected Task On(AddPattern command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanAdd(a.State.Patterns, command);
a.AddPattern(command);
});
}
protected Task On(DeletePattern command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanDelete(a.State.Patterns, command);
a.DeletePattern(command);
});
}
protected async Task On(UpdatePattern command, CommandContext context)
{
await handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanUpdate(a.State.Patterns, command);
a.UpdatePattern(command);
});
}
protected Task On(ChangePlan command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, async a =>

64
src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs

@ -21,15 +21,29 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class AppDomainObject : DomainObjectBase<AppState>
{
private readonly InitialPatterns initialPatterns;
public AppDomainObject(InitialPatterns initialPatterns)
{
Guard.NotNull(initialPatterns, nameof(initialPatterns));
this.initialPatterns = initialPatterns;
}
public AppDomainObject Create(CreateApp command)
{
ThrowIfCreated();
var appId = new NamedId<Guid>(command.AppId, command.Name);
RaiseEvent(SimpleMapper.Map(command, CreateInitalEvent(appId)));
RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command)));
RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId)));
RaiseEvent(CreateInitalEvent(appId, command.Actor, command.Name));
RaiseEvent(CreateInitialOwner(appId, command.Actor));
RaiseEvent(CreateInitialLanguage(appId, command.Actor));
foreach (var pattern in initialPatterns)
{
RaiseEvent(CreateInitialPattern(appId, command.Actor, pattern.Key, pattern.Value));
}
return this;
}
@ -123,6 +137,33 @@ namespace Squidex.Domain.Apps.Entities.Apps
return this;
}
public AppDomainObject AddPattern(AddPattern command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded()));
return this;
}
public AppDomainObject DeletePattern(DeletePattern command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted()));
return this;
}
public AppDomainObject UpdatePattern(UpdatePattern command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated()));
return this;
}
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)
@ -133,19 +174,24 @@ namespace Squidex.Domain.Apps.Entities.Apps
RaiseEvent(Envelope.Create(@event));
}
private static AppCreated CreateInitalEvent(NamedId<Guid> appId)
private static AppCreated CreateInitalEvent(NamedId<Guid> appId, RefToken actor, string name)
{
return new AppCreated { AppId = appId, Actor = actor, Name = name };
}
private static AppPatternAdded CreateInitialPattern(NamedId<Guid> appId, RefToken actor, Guid id, AppPattern p)
{
return new AppCreated { AppId = appId };
return new AppPatternAdded { AppId = appId, Actor = actor, PatternId = id, Name = p.Name, Pattern = p.Pattern, Message = p.Message };
}
private static AppLanguageAdded CreateInitialLanguage(NamedId<Guid> id)
private static AppLanguageAdded CreateInitialLanguage(NamedId<Guid> appId, RefToken actor)
{
return new AppLanguageAdded { AppId = id, Language = Language.EN };
return new AppLanguageAdded { AppId = appId, Actor = actor, Language = Language.EN };
}
private static AppContributorAssigned CreateInitialOwner(NamedId<Guid> id, SquidexCommand command)
private static AppContributorAssigned CreateInitialOwner(NamedId<Guid> appId, RefToken actor)
{
return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = AppContributorPermission.Owner };
return new AppContributorAssigned { AppId = appId, Actor = actor, ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner };
}
private void ThrowIfNotCreated()

28
src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs

@ -0,0 +1,28 @@
// ==========================================================================
// AddPattern.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class AddPattern : AppAggregateCommand
{
public Guid PatternId { get; set; }
public string Name { get; set; }
public string Pattern { get; set; }
public string Message { get; set; }
public AddPattern()
{
PatternId = Guid.NewGuid();
}
}
}

4
src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs

@ -13,10 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class CreateApp : SquidexCommand, IAggregateCommand
{
public string Name { get; set; }
public Guid AppId { get; set; }
public string Name { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return AppId; }

17
src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs

@ -0,0 +1,17 @@
// ==========================================================================
// DeletePattern.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class DeletePattern : AppAggregateCommand
{
public Guid PatternId { get; set; }
}
}

23
src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs

@ -0,0 +1,23 @@
// ==========================================================================
// UpdatePattern.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class UpdatePattern : AppAggregateCommand
{
public Guid PatternId { get; set; }
public string Name { get; set; }
public string Pattern { get; set; }
public string Message { get; set; }
}
}

97
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs

@ -0,0 +1,97 @@
// ==========================================================================
// GuardAppPattern.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public static class GuardAppPattern
{
public static void CanAdd(AppPatterns patterns, AddPattern command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot add pattern.", error =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
error(new ValidationError("Pattern name can not be empty.", nameof(command.Name)));
}
if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase)))
{
error(new ValidationError("Pattern name is already assigned.", nameof(command.Name)));
}
if (string.IsNullOrWhiteSpace(command.Pattern))
{
error(new ValidationError("Pattern can not be empty.", nameof(command.Pattern)));
}
else if (!command.Pattern.IsValidRegex())
{
error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern)));
}
if (patterns.Values.Any(x => x.Pattern == command.Pattern))
{
error(new ValidationError("Pattern already exists.", nameof(command.Pattern)));
}
});
}
public static void CanDelete(AppPatterns patterns, DeletePattern command)
{
Guard.NotNull(command, nameof(command));
if (!patterns.ContainsKey(command.PatternId))
{
throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern));
}
}
public static void CanUpdate(AppPatterns patterns, UpdatePattern command)
{
Guard.NotNull(command, nameof(command));
if (!patterns.ContainsKey(command.PatternId))
{
throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern));
}
Validate.It(() => "Cannot update pattern.", error =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
error(new ValidationError("Pattern name can not be empty.", nameof(command.Name)));
}
if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase)))
{
error(new ValidationError("Pattern name is already assigned.", nameof(command.Name)));
}
if (string.IsNullOrWhiteSpace(command.Pattern))
{
error(new ValidationError("Pattern can not be empty.", nameof(command.Pattern)));
}
else if (!command.Pattern.IsValidRegex())
{
error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern)));
}
if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern))
{
error(new ValidationError("Pattern already exists.", nameof(command.Pattern)));
}
});
}
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs

@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
AppClients Clients { get; }
AppPatterns Patterns { get; }
AppContributors Contributors { get; }
LanguagesConfig LanguagesConfig { get; }

26
src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs

@ -0,0 +1,26 @@
// ==========================================================================
// InitialPatterns.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class InitialPatterns : Dictionary<Guid, AppPattern>
{
public InitialPatterns()
{
}
public InitialPatterns(Dictionary<Guid, AppPattern> patterns)
: base(patterns)
{
}
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs

@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Repositories
{
Task<Guid> FindAppIdByNameAsync(string name);
Task<IReadOnlyList<Guid>> QueryAppIdsAsync();
Task<IReadOnlyList<Guid>> QueryUserAppIdsAsync(string userId);
}
}

18
src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -31,6 +31,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
[JsonProperty]
public AppClients Clients { get; set; } = AppClients.Empty;
[JsonProperty]
public AppPatterns Patterns { get; set; } = AppPatterns.Empty;
[JsonProperty]
public AppContributors Contributors { get; set; } = AppContributors.Empty;
@ -77,6 +80,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
Clients = Clients.Revoke(@event.Id);
}
protected void On(AppPatternAdded @event)
{
Patterns = Patterns.Add(@event.PatternId, @event.Name, @event.Pattern, @event.Message);
}
protected void On(AppPatternDeleted @event)
{
Patterns = Patterns.Remove(@event.PatternId);
}
protected void On(AppPatternUpdated @event)
{
Patterns = Patterns.Update(@event.PatternId, @event.Name, @event.Pattern, @event.Message);
}
protected void On(AppLanguageAdded @event)
{
LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language));

25
src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs

@ -0,0 +1,25 @@
// ==========================================================================
// AppPatternAdded.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppPatternAdded))]
public sealed class AppPatternAdded : AppEvent
{
public Guid PatternId { get; set; }
public string Name { get; set; }
public string Pattern { get; set; }
public string Message { get; set; }
}
}

21
src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs

@ -0,0 +1,21 @@
// ==========================================================================
// AppPatternDeleted.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppPatternDeleted))]
public sealed class AppPatternDeleted : AppEvent
{
public Guid PatternId { get; set; }
public string Name { get; set; }
}
}

25
src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs

@ -0,0 +1,25 @@
// ==========================================================================
// AppPatternUpdated.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppPatternUpdated))]
public sealed class AppPatternUpdated : AppEvent
{
public Guid PatternId { get; set; }
public string Name { get; set; }
public string Pattern { get; set; }
public string Message { get; set; }
}
}

2
src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs

@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Migrations
await Collection.FindOneAndUpdateAsync<MongoMigrationEntity>(x => x.Id == DefaultId,
Update
.Set(x => x.IsLocked, true)
.Set(x => x.Version, 0),
.SetOnInsert(x => x.Version, 0),
UpsertFind);
return entity == null || entity.IsLocked == false;

130
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -0,0 +1,130 @@
// ==========================================================================
// AppPatternsController.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures app patterns.
/// </summary>
[ApiAuthorize]
[MustBeAppDeveloper]
[ApiExceptionFilter]
[AppApi]
[SwaggerTag(nameof(Apps))]
public sealed class AppPatternsController : ApiController
{
public AppPatternsController(ICommandBus commandBus)
: base(commandBus)
{
}
/// <summary>
/// Get app patterns.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Patterns returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// Gets all configured regex patterns for the app with the specified name.
/// </remarks>
[HttpGet]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto[]), 200)]
[ApiCosts(0)]
public IActionResult GetPatterns(string app)
{
var response =
App.Patterns.Select(x => SimpleMapper.Map(x.Value, new AppPatternDto { PatternId = x.Key }))
.OrderBy(x => x.Name).ToList();
return Ok(response);
}
/// <summary>
/// Create a new app patterm.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">Pattern to be added to the app.</param>
/// <returns>
/// 201 => Pattern generated.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto), 201)]
[ApiCosts(1)]
public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request)
{
var command = SimpleMapper.Map(request, new AddPattern());
await CommandBus.PublishAsync(command);
var response = SimpleMapper.Map(request, new AppPatternDto { PatternId = command.PatternId });
return CreatedAtAction(nameof(GetPatterns), new { app }, request);
}
/// <summary>
/// Update an existing app patterm.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the pattern to be updated.</param>
/// <param name="request">Pattern to be updated for the app.</param>
/// <returns>
/// 204 => Pattern updated.
/// 404 => App not found or pattern not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(AppPatternDto), 201)]
[ApiCosts(1)]
public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request)
{
var command = SimpleMapper.Map(request, new UpdatePattern { PatternId = id });
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Revoke an app client
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the pattern to be deleted.</param>
/// <returns>
/// 204 => Pattern removed.
/// 404 => App or pattern not found.
/// </returns>
/// <remarks>
/// Schemas using this pattern will still function using the same Regular Expression
/// </remarks>
[HttpDelete]
[Route("apps/{app}/patterns/{id}/")]
[ApiCosts(1)]
public async Task<IActionResult> DeletePattern(string app, Guid id)
{
await CommandBus.PublishAsync(new DeletePattern { PatternId = id });
return NoContent();
}
}
}

38
src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs

@ -0,0 +1,38 @@
// ==========================================================================
// AppPatternDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppPatternDto
{
/// <summary>
/// Identifier for Pattern
/// </summary>
public Guid PatternId { get; set; }
/// <summary>
/// The name of the suggestion.
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// The regex pattern.
/// </summary>
[Required]
public string Pattern { get; set; }
/// <summary>
/// The regex message.
/// </summary>
public string Message { get; set; }
}
}

11
src/Squidex/Areas/Api/Controllers/UI/Models/UIRegexSuggestionDto.cs → src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs

@ -1,5 +1,5 @@
// ==========================================================================
// UIRegexSuggestionDto.cs
// AddPatternDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,9 +8,9 @@
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.UI.Models
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class UIRegexSuggestionDto
public class UpdatePatternDto
{
/// <summary>
/// The name of the suggestion.
@ -23,5 +23,10 @@ namespace Squidex.Areas.Api.Controllers.UI.Models
/// </summary>
[Required]
public string Pattern { get; set; }
/// <summary>
/// The regex message.
/// </summary>
public string Message { get; set; }
}
}

7
src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs

@ -6,19 +6,12 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.UI.Models
{
public sealed class UISettingsDto
{
/// <summary>
/// The regex suggestions.
/// </summary>
[Required]
public List<UIRegexSuggestionDto> RegexSuggestions { get; set; }
/// <summary>
/// The type of the map control.
/// </summary>

9
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -6,8 +6,6 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NSwag.Annotations;
@ -44,13 +42,6 @@ namespace Squidex.Areas.Api.Controllers.UI
{
var dto = new UISettingsDto
{
RegexSuggestions =
uiOptions.RegexSuggestions?
.Where(x =>
!string.IsNullOrWhiteSpace(x.Key) &&
!string.IsNullOrWhiteSpace(x.Value))
.Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }).ToList()
?? new List<UIRegexSuggestionDto>(),
MapType = uiOptions.Map?.Type ?? "OSM",
MapKey = uiOptions.Map?.GoogleMaps?.Key
};

3
src/Squidex/Config/Domain/SerializationServices.cs

@ -29,7 +29,7 @@ namespace Squidex.Config.Domain
{
private static readonly TypeNameRegistry TypeNameRegistry =
new TypeNameRegistry()
.MapUnmapped(typeof(Migration01).Assembly)
.MapUnmapped(typeof(Migration01_FromCqrs).Assembly)
.MapUnmapped(typeof(SquidexCoreModel).Assembly)
.MapUnmapped(typeof(SquidexEvents).Assembly)
.MapUnmapped(typeof(SquidexInfrastructure).Assembly);
@ -43,6 +43,7 @@ namespace Squidex.Config.Domain
settings.ContractResolver = new ConverterContractResolver(
new AppClientsConverter(),
new AppContributorsConverter(),
new AppPatternsConverter(),
new ClaimsPrincipalConverter(),
new InstantConverter(),
new LanguageConverter(),

26
src/Squidex/Config/Domain/WriteServices.cs

@ -6,8 +6,11 @@
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Migrate_01;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
@ -61,7 +64,10 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<Migration01>()
services.AddTransientAs<Migration01_FromCqrs>()
.As<IMigration>();
services.AddTransientAs<Migration02_AddPatterns>()
.As<IMigration>();
services.AddTransientAs<AppDomainObject>()
@ -78,6 +84,24 @@ namespace Squidex.Config.Domain
services.AddTransientAs<SchemaDomainObject>()
.AsSelf();
services.AddSingleton<InitialPatterns>(c =>
{
var config = c.GetRequiredService<IOptions<MyUIOptions>>();
var result = new InitialPatterns();
foreach (var pattern in config.Value.RegexSuggestions)
{
if (!string.IsNullOrWhiteSpace(pattern.Key) &&
!string.IsNullOrWhiteSpace(pattern.Value))
{
result[Guid.NewGuid()] = new AppPattern(pattern.Key, pattern.Value);
}
}
return result;
});
}
}
}

2
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -139,7 +139,7 @@
<sqx-number-validation [editForm]="editForm" [properties]="field.properties"></sqx-number-validation>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-validation [editForm]="editForm" [properties]="field.properties"></sqx-string-validation>
<sqx-string-validation [editForm]="editForm" [properties]="field.properties" [regexSuggestions]="regexSuggestions"></sqx-string-validation>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-validation [editForm]="editForm" [properties]="field.properties"></sqx-boolean-validation>

4
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -9,6 +9,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import {
AppPatternDto,
createProperties,
fadeAnimation,
FieldDto,
@ -31,6 +32,9 @@ export class FieldComponent implements OnInit {
@Input()
public schemas: SchemaDto[];
@Input()
public regexSuggestions: AppPatternDto[] = [];
@Output()
public locking = new EventEmitter();

1
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -62,6 +62,7 @@
<div class="panel-content panel-content-scroll" dnd-sortable-container [sortableData]="schema.fields">
<div *ngFor="let field of schema.fields; let i = index" dnd-sortable [sortableIndex]="i" (sqxSorted)="sortFields($event)">
<sqx-field [field]="field" [schemas]="schemas?.values"
[regexSuggestions]="regexSuggestions"
(disabling)="disableField(field)"
(deleting)="deleteField(field)"
(enabling)="enableField(field)"

12
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -13,6 +13,8 @@ import { Subscription } from 'rxjs';
import {
AddFieldDto,
AppContext,
AppPatternDto,
AppPatternsService,
createProperties,
fadeAnimation,
FieldDto,
@ -57,6 +59,8 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
public schema: SchemaDetailsDto;
public schemas: ImmutableArray<SchemaDto>;
public regexSuggestions: AppPatternDto[] = [];
public exportSchemaDialog = new ModalView();
public configureScriptsDialog = new ModalView();
@ -86,7 +90,8 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
constructor(public readonly ctx: AppContext,
private readonly formBuilder: FormBuilder,
private readonly router: Router,
private readonly schemasService: SchemasService
private readonly schemasService: SchemasService,
private readonly appPatternsService: AppPatternsService
) {
}
@ -114,6 +119,11 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
}
private load() {
this.appPatternsService.getPatterns(this.ctx.appName)
.subscribe(dtos => {
this.regexSuggestions = dtos.patterns;
});
this.schemasService.getSchemas(this.ctx.appName)
.subscribe(dtos => {
this.schemas = ImmutableArray.of(dtos);

7
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html

@ -33,14 +33,17 @@
<div *ngIf="regexSuggestions.length > 0 && (regexSuggestionsModal.isOpen | async) && (showPatternSuggestions | async)" [sqxModalTarget]="patternInput" class="control-dropdown">
<h4>Suggestions</h4>
<div *ngFor="let suggestion of regexSuggestions" class="control-dropdown-item control-dropdown-item-selectable" (mousedown)="setPattern(suggestion.pattern)">
<div *ngFor="let suggestion of regexSuggestions" class="control-dropdown-item control-dropdown-item-selectable" (mousedown)="setPattern(suggestion)">
<div class="truncate">{{suggestion.name}}</div>
<div class="truncate text-muted">{{suggestion.pattern}}</div>
</div>
</div>
</div>
<small class="col col-3" style="align-self: center;">
{{patternName}}
</small>
</div>
<div class="form-group row" *ngIf="showPatternMessage | async">
<div class="form-group row" *ngIf="showPatternMessage">
<label class="col col-3 col-form-label" for="field-pattern-message">Pattern Message</label>
<div class="col col-6">

52
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -10,10 +10,9 @@ import { FormControl, FormGroup } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import {
AppPatternDto,
ModalView,
StringFieldPropertiesDto,
UIRegexSuggestionDto,
UIService
StringFieldPropertiesDto
} from 'shared';
@Component({
@ -23,7 +22,6 @@ import {
})
export class StringValidationComponent implements OnDestroy, OnInit {
private patternSubscription: Subscription;
private uiSettingsSubscription: Subscription;
@Input()
public editForm: FormGroup;
@ -31,22 +29,18 @@ export class StringValidationComponent implements OnDestroy, OnInit {
@Input()
public properties: StringFieldPropertiesDto;
@Input()
public regexSuggestions: AppPatternDto[] = [];
public showDefaultValue: Observable<boolean>;
public showPatternMessage: Observable<boolean>;
public showPatternMessage: boolean;
public showPatternSuggestions: Observable<boolean>;
public regexSuggestions: UIRegexSuggestionDto[] = [];
public patternName: string;
public regexSuggestionsModal = new ModalView(false, false);
constructor(
private readonly uiService: UIService
) {
}
public ngOnDestroy() {
this.patternSubscription.unsubscribe();
this.uiSettingsSubscription.unsubscribe();
}
public ngOnInit() {
@ -71,31 +65,41 @@ export class StringValidationComponent implements OnDestroy, OnInit {
.map(x => !x);
this.showPatternMessage =
this.editForm.controls['pattern'].valueChanges
.startWith('')
.map(x => x && x.trim().length > 0);
this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0;
this.showPatternSuggestions =
this.editForm.controls['pattern'].valueChanges
.startWith('')
.map(x => !x || x.trim().length === 0);
this.uiSettingsSubscription =
this.uiService.getSettings()
.subscribe(settings => {
this.regexSuggestions = settings.regexSuggestions;
});
this.patternSubscription =
this.editForm.controls['pattern'].valueChanges
.subscribe((value: string) => {
if (!value || value.length === 0) {
this.editForm.controls['patternMessage'].setValue(undefined);
}
this.setPatternName();
});
this.setPatternName();
}
public setPattern(pattern: string) {
this.editForm.controls['pattern'].setValue(pattern);
public setPattern(pattern: AppPatternDto) {
this.patternName = pattern.name;
this.editForm.controls['pattern'].setValue(pattern.pattern);
this.editForm.controls['patternMessage'].setValue(pattern.message);
this.showPatternMessage = true;
}
private setPatternName() {
const matchingPattern = this.regexSuggestions.find(x => x.pattern === this.editForm.controls['pattern'].value);
if (matchingPattern) {
this.patternName = matchingPattern.name;
} else if (this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim() !== '') {
this.patternName = 'Advanced';
} else {
this.patternName = undefined;
}
}
}

2
src/Squidex/app/features/settings/declarations.ts

@ -10,6 +10,8 @@ export * from './pages/clients/clients-page.component';
export * from './pages/contributors/contributors-page.component';
export * from './pages/languages/language.component';
export * from './pages/languages/languages-page.component';
export * from './pages/patterns/pattern.component';
export * from './pages/patterns/patterns-page.component';
export * from './pages/plans/plans-page.component';
export * from './settings-area.component';

24
src/Squidex/app/features/settings/module.ts

@ -22,6 +22,8 @@ import {
ContributorsPageComponent,
LanguageComponent,
LanguagesPageComponent,
PatternComponent,
PatternsPageComponent,
PlansPageComponent,
SettingsAreaComponent
} from './declarations';
@ -97,6 +99,26 @@ const routes: Routes = [
}
}
]
},
{
path: 'patterns',
component: PatternsPageComponent,
children: [
{
path: 'history',
component: HistoryComponent,
data: {
channel: 'settings.patterns'
}
},
{
path: 'help',
component: HelpComponent,
data: {
helpPage: '05-integrated/patterns'
}
}
]
}
]
}
@ -115,6 +137,8 @@ const routes: Routes = [
ContributorsPageComponent,
LanguageComponent,
LanguagesPageComponent,
PatternComponent,
PatternsPageComponent,
PlansPageComponent,
SettingsAreaComponent
]

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

@ -82,7 +82,7 @@ export class LanguagesPageComponent implements OnInit {
}
public updateLanguage(language: AppLanguageDto) {
this.appLanguagesService.updateLanguage(this.ctx.appName, language.iso2Code, language, this.appLanguages.version)
this.appLanguagesService.putLanguage(this.ctx.appName, language.iso2Code, language, this.appLanguages.version)
.subscribe(dto => {
this.updateLanguages(this.appLanguages.updateLanguage(language, dto.version));
}, error => {

45
src/Squidex/app/features/settings/pages/patterns/pattern.component.html

@ -0,0 +1,45 @@
<div class="table-items-row" [class.table-items-footer]="!pattern">
<form [formGroup]="editForm" (ngSubmit)="save()" class="row no-gutters">
<div class="col col-name">
<sqx-control-errors for="name" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="pattern-name" maxlength="100" formControlName="name" placeholder="Name" />
</div>
<div class="col pl-2 pr-2">
<sqx-control-errors for="pattern" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="pattern-pattern" maxlength="1000" formControlName="pattern" placeholder="Pattern" />
</div>
<div class="col col-message">
<sqx-control-errors for="message" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="pattern-message" maxlength="1000" formControlName="message" placeholder="Message" />
</div>
<div class="col col-auto pl-2 col-options" *ngIf="pattern">
<button type="submit" class="btn btn-primary" [class.disabled]="!editForm.dirty">
<i class="icon-checkmark"></i>
</button>
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="removing.emit(pattern)"
confirmTitle="Remove pattern"
confirmText="Do you really want to remove this pattern?">
<i class="icon-bin2"></i>
</button>
</div>
<div class="col col-auto pl-2 col-options" *ngIf="!pattern">
<button type="submit" class="btn btn-success">
<i class="icon-add"></i>
</button>
<button type="reset" class="btn btn-link btn-decent" (click)="cancel()">
<i class="icon-close"></i>
</button>
</div>
</form>
</div>

13
src/Squidex/app/features/settings/pages/patterns/pattern.component.scss

@ -0,0 +1,13 @@
@import '_vars';
@import '_mixins';
.col-options {
min-width: 7.5rem;
max-width: 7.5rem;
}
.col-name,
.col-message {
min-width: 10rem;
max-width: 10rem;
}

99
src/Squidex/app/features/settings/pages/patterns/pattern.component.ts

@ -0,0 +1,99 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import {
AppPatternDto,
fadeAnimation,
ValidatorsEx,
UpdatePatternDto
} from 'shared';
@Component({
selector: 'sqx-pattern',
styleUrls: ['./pattern.component.scss'],
templateUrl: './pattern.component.html',
animations: [
fadeAnimation
]
})
export class PatternComponent implements OnInit {
@Input()
public isNew = false;
@Input()
public pattern: AppPatternDto;
@Output()
public removing = new EventEmitter<any>();
@Output()
public updating = new EventEmitter<UpdatePatternDto>();
public editFormSubmitted = false;
public editForm =
this.formBuilder.group({
name: [
'',
[
Validators.required,
Validators.maxLength(100),
ValidatorsEx.pattern('[A-z0-9]+[A-z0-9\- ]*[A-z0-9]', 'Name can only contain letters, numbers, dashes and spaces.')
]
],
pattern: [
'',
[
Validators.required
]
],
message: [
'',
[
Validators.maxLength(1000)
]
]
});
constructor(
private readonly formBuilder: FormBuilder
) {
}
public ngOnInit() {
const pattern = this.pattern;
if (pattern) {
this.editForm.setValue({ name: pattern.name, pattern: pattern.pattern, message: pattern.message || '' });
}
}
public cancel() {
this.editFormSubmitted = false;
this.editForm.reset();
}
public save() {
this.editFormSubmitted = true;
if (this.editForm.valid) {
const requestDto = new UpdatePatternDto(
this.editForm.controls['name'].value,
this.editForm.controls['pattern'].value,
this.editForm.controls['message'].value);
this.updating.emit(requestDto);
if (!this.pattern) {
this.cancel();
}
}
}
}

41
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html

@ -0,0 +1,41 @@
<sqx-title message="{app} | Patterns | Settings" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="60rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title">Patterns</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div *ngIf="appPatterns">
<div *ngFor="let pattern of appPatterns.patterns">
<sqx-pattern [pattern]="pattern"
(removing)="removePattern(pattern)"
(updating)="updatePattern(pattern, $event)">
</sqx-pattern>
</div>
<sqx-pattern [isNew]="true"
(updating)="addPattern($event)">
</sqx-pattern>
</div>
</div>
<div class="panel-sidebar">
<a class="panel-link" routerLink="history" routerLinkActive="active">
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="help" routerLinkActive="active">
<i class="icon-help"></i>
</a>
</div>
</div>
</sqx-panel>
<router-outlet></router-outlet>

2
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

87
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, OnInit } from '@angular/core';
import {
AppContext,
AppPatternDto,
AppPatternsDto,
AppPatternsService,
HistoryChannelUpdated,
UpdatePatternDto
} from 'shared';
@Component({
selector: 'sqx-patterns-page',
styleUrls: ['./patterns-page.component.scss'],
templateUrl: './patterns-page.component.html',
providers: [
AppContext
]
})
export class PatternsPageComponent implements OnInit {
public appPatterns: AppPatternsDto;
constructor(public readonly ctx: AppContext,
private readonly appPatternsService: AppPatternsService
) {
}
public ngOnInit() {
this.load();
}
public load() {
this.appPatternsService.getPatterns(this.ctx.appName).retry(2)
.subscribe(dtos => {
this.updatePatterns(dtos);
}, error => {
this.ctx.notifyError(error);
});
}
public addPattern(pattern: AppPatternDto) {
const requestDto = new UpdatePatternDto(pattern.name, pattern.pattern, pattern.message);
this.appPatternsService.postPattern(this.ctx.appName, requestDto, this.appPatterns.version)
.subscribe(dto => {
this.updatePatterns(this.appPatterns.addPattern(dto.payload, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public updatePattern(pattern: AppPatternDto, update: UpdatePatternDto) {
this.appPatternsService.putPattern(this.ctx.appName, pattern.patternId, update, this.appPatterns.version)
.subscribe(dto => {
this.updatePatterns(this.appPatterns.updatePattern(pattern.update(update), dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public removePattern(pattern: AppPatternDto) {
this.appPatternsService.deletePattern(this.ctx.appName, pattern.patternId, this.appPatterns.version)
.subscribe(dto => {
this.updatePatterns(this.appPatterns.deletePattern(pattern, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
private updatePatterns(patterns: AppPatternsDto) {
this.appPatterns =
new AppPatternsDto(
patterns.patterns.sort((a, b) => {
return a.name.localeCompare(b.name);
}),
patterns.version);
this.ctx.bus.emit(new HistoryChannelUpdated());
}
}

6
src/Squidex/app/features/settings/settings-area.component.html

@ -32,6 +32,12 @@
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="patterns" routerLinkActive="active">
Patterns
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="ctx.app.permission === 'Owner'">
<a class="nav-link" routerLink="plans" routerLinkActive="active">
Update Plan

1
src/Squidex/app/shared/declarations-base.ts

@ -20,6 +20,7 @@ export * from './interceptors/auth.interceptor';
export * from './services/app-contributors.service';
export * from './services/app-clients.service';
export * from './services/app-languages.service';
export * from './services/app-patterns.service';
export * from './services/apps-store.service';
export * from './services/apps.service';
export * from './services/assets.service';

2
src/Squidex/app/shared/module.ts

@ -17,6 +17,7 @@ import {
AppContributorsService,
AppLanguagesService,
AppMustExistGuard,
AppPatternsService,
AppsStoreService,
AppsService,
AssetComponent,
@ -118,6 +119,7 @@ export class SqxSharedModule {
AppContributorsService,
AppLanguagesService,
AppMustExistGuard,
AppPatternsService,
AppsService,
AppsStoreService,
AssetsService,

2
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -172,7 +172,7 @@ describe('AppLanguagesService', () => {
const dto = new UpdateAppLanguageDto(true, true, []);
appLanguagesService.updateLanguage('my-app', 'de', dto, version).subscribe();
appLanguagesService.putLanguage('my-app', 'de', dto, version).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages/de');

2
src/Squidex/app/shared/services/app-languages.service.ts

@ -141,7 +141,7 @@ export class AppLanguagesService {
.pretifyError('Failed to add language. Please reload.');
}
public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable<Versioned<any>> {
public putLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return HTTP.putVersioned(this.http, url, dto, version)

174
src/Squidex/app/shared/services/app-patterns.service.spec.ts

@ -0,0 +1,174 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
AppPatternDto,
AppPatternsDto,
AppPatternsService,
UpdatePatternDto,
Version
} from './../';
describe('ApppatternsDto', () => {
const pattern1 = new AppPatternDto('1', 'Any', '.*', 'Message1');
const pattern2 = new AppPatternDto('2', 'Number', '[0-9]', 'Message2');
const pattern2_new = new AppPatternDto('2', 'Numbers', '[0-9]*', 'Message2_1');
const version = new Version('1');
const newVersion = new Version('2');
it('should update patterns when adding pattern', () => {
const patterns_1 = new AppPatternsDto([pattern1], version);
const patterns_2 = patterns_1.addPattern(pattern2, newVersion);
expect(patterns_2.patterns).toEqual([pattern1, pattern2]);
expect(patterns_2.version).toEqual(newVersion);
});
it('should update patterns when removing pattern', () => {
const patterns_1 = new AppPatternsDto([pattern1, pattern2], version);
const patterns_2 = patterns_1.deletePattern(pattern1, newVersion);
expect(patterns_2.patterns).toEqual([pattern2]);
expect(patterns_2.version).toEqual(newVersion);
});
it('should update patterns when updating pattern', () => {
const patterns_1 = new AppPatternsDto([pattern1, pattern2], version);
const patterns_2 = patterns_1.updatePattern(pattern2_new, newVersion);
expect(patterns_2.patterns).toEqual([pattern1, pattern2_new]);
expect(patterns_2.version).toEqual(newVersion);
});
});
describe('AppPatternDto', () => {
it('should update properties when updating', () => {
const pattern_1 = new AppPatternDto('1', 'Number', '[0-9]', 'Message1');
const pattern_2 = pattern_1.update(new UpdatePatternDto('Numbers', '[0-9]*', 'Message2'));
expect(pattern_2.name).toBe('Numbers');
expect(pattern_2.pattern).toBe('[0-9]*');
expect(pattern_2.message).toBe('Message2');
});
});
describe('AppPatternsService', () => {
const version = new Version('1');
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
AppPatternsService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get patterns',
inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => {
let patterns: AppPatternsDto | null = null;
patternService.getPatterns('my-app').subscribe(result => {
patterns = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([
{
patternId: '1',
pattern: '[0-9]',
name: 'Number',
message: 'Message1'
}, {
patternId: '2',
pattern: '[0-9]*',
name: 'Numbers',
message: 'Message2'
}
], {
headers: {
etag: '2'
}
});
expect(patterns).toEqual(
new AppPatternsDto([
new AppPatternDto('1', 'Number', '[0-9]', 'Message1'),
new AppPatternDto('2', 'Numbers', '[0-9]*', 'Message2')
], new Version('2')));
}));
it('should make post request to add pattern',
inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => {
const dto = new UpdatePatternDto('Number', '[0-9]', 'Message1');
let pattern: AppPatternDto | null = null;
patternService.postPattern('my-app', dto, version).subscribe(result => {
pattern = result.payload;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({
patternId: '1',
pattern: '[0-9]',
name: 'Number',
message: 'Message1'
});
expect(pattern).toEqual(new AppPatternDto('1', 'Number', '[0-9]', 'Message1'));
}));
it('should make put request to update pattern',
inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => {
const dto = new UpdatePatternDto('Number', '[0-9]', 'Message1');
patternService.putPattern('my-app', '1', dto, version).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({});
}));
it('should make delete request to remove pattern',
inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => {
patternService.deletePattern('my-app', '1', version).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({});
}));
});

130
src/Squidex/app/shared/services/app-patterns.service.ts

@ -0,0 +1,130 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import {
ApiUrlConfig,
HTTP,
Version,
Versioned
} from 'framework';
export class AppPatternsDto {
constructor(
public readonly patterns: AppPatternDto[],
public readonly version: Version
) {
}
public addPattern(pattern: AppPatternDto, version: Version) {
return new AppPatternsDto([...this.patterns, pattern], version);
}
public updatePattern(pattern: AppPatternDto, version: Version) {
return new AppPatternsDto(this.patterns.map(p => p.patternId === pattern.patternId ? pattern : p), version);
}
public deletePattern(pattern: AppPatternDto, version: Version) {
return new AppPatternsDto(this.patterns.filter(c => c.patternId !== pattern.patternId), version);
}
}
export class AppPatternDto {
constructor(
public readonly patternId: string,
public readonly name: string,
public readonly pattern: string,
public readonly message: string
) {
}
public update(update: UpdatePatternDto) {
return new AppPatternDto(
this.patternId,
update.name,
update.pattern,
update.message);
}
}
export class UpdatePatternDto {
constructor(
public readonly name: string,
public readonly pattern: string,
public readonly message: string
) {
}
}
@Injectable()
export class AppPatternsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getPatterns(appName: string): Observable<AppPatternsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
const items: any[] = body;
return new AppPatternsDto(
items.map(item => {
return new AppPatternDto(
item.patternId,
item.name,
item.pattern,
item.message);
}),
response.version);
})
.pretifyError('Failed to add pattern. Please reload.');
}
public postPattern(appName: string, dto: UpdatePatternDto, version: Version): Observable<Versioned<AppPatternDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`);
return HTTP.postVersioned<any>(this.http, url, dto, version)
.map(response => {
const body = response.payload.body;
const pattern = new AppPatternDto(
body.patternId,
body.name,
body.pattern,
body.message);
return new Versioned(response.version, pattern);
})
.pretifyError('Failed to add pattern. Please reload.');
}
public putPattern(appName: string, id: string, dto: UpdatePatternDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns/${id}`);
return HTTP.putVersioned(this.http, url, dto, version)
.pretifyError('Failed to update pattern. Please reload.');
}
public deletePattern(appName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns/${id}`);
return HTTP.deleteVersioned(this.http, url, version)
.pretifyError('Failed to remove pattern. Please reload.');
}
}

3
src/Squidex/app/shared/services/ui.service.spec.ts

@ -41,7 +41,7 @@ describe('UIService', () => {
settings1 = result;
});
const response: UISettingsDto = { regexSuggestions: [], mapType: 'OSM', mapKey: '' };
const response: UISettingsDto = { mapType: 'OSM', mapKey: '' };
const req = httpMock.expectOne('http://service/p/api/ui/settings');
@ -75,6 +75,5 @@ describe('UIService', () => {
req.error(new ErrorEvent('500'));
expect(settings).toBeDefined();
expect(settings!.regexSuggestions).toEqual([]);
}));
});

5
src/Squidex/app/shared/services/ui.service.ts

@ -14,15 +14,10 @@ import 'framework/angular/http-extensions';
import { ApiUrlConfig } from 'framework';
export interface UISettingsDto {
regexSuggestions: UIRegexSuggestionDto[];
mapType: string;
mapKey: string;
}
export interface UIRegexSuggestionDto {
name: string; pattern: string;
}
@Injectable()
export class UIService {
private settings: UISettingsDto;

2
src/Squidex/appsettings.json

@ -16,7 +16,7 @@
// Regex for phone numbers.
"Phone": "^\\(*\\+*[1-9]{0,3}\\)*-*[1-9]{0,3}[-. /]*\\(*[2-9]\\d{2}\\)*[-. /]*\\d{3}[-. /]*\\d{4} *e*x*t*\\.* *\\d{0,4}$",
// Regex for slugs (e.g. hello-world).
"Slug": "^[a-z0-9]+(\\\\-[a-z0-9]+)*$",
"Slug": "^[a-z0-9]+(\\-[a-z0-9]+)*$",
// Regex for urls.
"Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$"
},

44
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs

@ -0,0 +1,44 @@
// ==========================================================================
// AppClientJsonTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using FluentAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Apps
{
public class AppPatternJsonTests
{
private readonly JsonSerializer serializer = TestData.DefaultSerializer();
[Fact]
public void Should_serialize_and_deserialize()
{
var patterns = AppPatterns.Empty;
var guid1 = Guid.NewGuid();
var guid2 = Guid.NewGuid();
var guid3 = Guid.NewGuid();
patterns = patterns.Add(guid1, "Name1", "Pattern1", "Default");
patterns = patterns.Add(guid2, "Name2", "Pattern2", "Default");
patterns = patterns.Add(guid3, "Name3", "Pattern3", "Default");
patterns = patterns.Update(guid2, "Name2 Update", "Pattern2 Update", "Default2");
patterns = patterns.Remove(guid1);
var appPatterns = JToken.FromObject(patterns, serializer).ToObject<AppPatterns>(serializer);
appPatterns.ShouldBeEquivalentTo(patterns);
}
}
}

77
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs

@ -0,0 +1,77 @@
// ==========================================================================
// AppClientsTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Xunit;
#pragma warning disable SA1310 // Field names must not contain underscore
namespace Squidex.Domain.Apps.Core.Model.Apps
{
public class AppPatternsTests
{
private readonly AppPatterns patterns_1;
private readonly Guid firstId = Guid.NewGuid();
private readonly Guid id = Guid.NewGuid();
public AppPatternsTests()
{
patterns_1 = AppPatterns.Empty.Add(firstId, "Default", "Default Pattern", "Message");
}
[Fact]
public void Should_add_pattern()
{
var patterns_2 = patterns_1.Add(id, "NewPattern", "New Pattern", "Message");
patterns_2[id].ShouldBeEquivalentTo(new AppPattern("NewPattern", "New Pattern", "Message"));
}
[Fact]
public void Should_throw_exception_if_add_pattern_with_same_id()
{
var patterns_2 = patterns_1.Add(id, "NewPattern", "New Pattern", "Message");
Assert.Throws<ArgumentException>(() => patterns_2.Add(id, "NewPattern", "New Pattern", "Message"));
}
[Fact]
public void Should_update_pattern()
{
var patterns_2 = patterns_1.Update(firstId, "UpdatePattern", "Update Pattern", "Message");
patterns_2[firstId].ShouldBeEquivalentTo(new AppPattern("UpdatePattern", "Update Pattern", "Message"));
}
[Fact]
public void Should_return_same_patterns_if_pattern_not_found()
{
var patterns_2 = patterns_1.Update(id, "NewPattern", "NewPattern", "Message");
Assert.Same(patterns_1, patterns_2);
}
[Fact]
public void Should_remove_pattern()
{
var patterns_2 = patterns_1.Remove(firstId);
Assert.Empty(patterns_2);
}
[Fact]
public void Should_do_nothing_if_remove_pattern_not_found()
{
var patterns_2 = patterns_1.Remove(id);
Assert.NotSame(patterns_1, patterns_2);
}
}
}

1
tests/Squidex.Domain.Apps.Core.Tests/TestData.cs

@ -35,6 +35,7 @@ namespace Squidex.Domain.Apps.Core
ContractResolver = new ConverterContractResolver(
new AppClientsConverter(),
new AppContributorsConverter(),
new AppPatternsConverter(),
new InstantConverter(),
new LanguageConverter(),
new LanguagesConfigConverter(),

44
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs

@ -26,10 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>();
private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>();
private readonly IUserResolver userResolver = A.Fake<IUserResolver>();
private readonly AppDomainObject app = new AppDomainObject();
private readonly Language language = Language.DE;
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientName = "client";
private readonly Guid patternId = Guid.NewGuid();
private readonly AppDomainObject app = new AppDomainObject(new InitialPatterns());
private readonly AppCommandMiddleware sut;
protected override Guid Id
@ -232,6 +233,47 @@ namespace Squidex.Domain.Apps.Entities.Apps
});
}
[Fact]
public async Task AddPattern_should_update_domain_object()
{
CreateApp();
var context = CreateContextForCommand(new AddPattern { Name = "Any", Pattern = ".*" });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task UpdatePattern_should_update_domain()
{
CreateApp()
.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = "." }));
var context = CreateContextForCommand(new UpdatePattern { PatternId = patternId, Name = "Number", Pattern = "[0-9]" });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task DeletePattern_should_update_domain_object()
{
CreateApp()
.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = "." }));
var context = CreateContextForCommand(new DeletePattern { PatternId = patternId });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
private AppDomainObject CreateApp()
{
app.Create(CreateCommand(new CreateApp { AppId = AppId, Name = AppName }));

101
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs

@ -24,7 +24,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly string clientId = "client";
private readonly string clientNewName = "My Client";
private readonly string planId = "premium";
private readonly AppDomainObject sut = new AppDomainObject();
private readonly Guid patternId = Guid.NewGuid();
private readonly AppDomainObject sut = new AppDomainObject(new InitialPatterns());
protected override Guid Id
{
@ -45,15 +46,28 @@ namespace Squidex.Domain.Apps.Entities.Apps
[Fact]
public void Create_should_specify_name_and_owner()
{
sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId }));
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
Assert.Equal(AppName, sut.State.Name);
var initialPatterns = new InitialPatterns
{
{ id1, new AppPattern("Number", "[0-9]") },
{ id2, new AppPattern("Numbers", "[0-9]*") }
};
sut.GetUncomittedEvents()
var app = new AppDomainObject(initialPatterns);
app.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId }));
Assert.Equal(AppName, app.State.Name);
app.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppCreated { Name = AppName }),
CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }),
CreateEvent(new AppLanguageAdded { Language = Language.EN })
CreateEvent(new AppLanguageAdded { Language = Language.EN }),
CreateEvent(new AppPatternAdded { PatternId = id1, Name = "Number", Pattern = "[0-9]" }),
CreateEvent(new AppPatternAdded { PatternId = id2, Name = "Numbers", Pattern = "[0-9]*" })
);
}
@ -281,6 +295,83 @@ namespace Squidex.Domain.Apps.Entities.Apps
);
}
[Fact]
public void AddPattern_should_throw_exception_if_app_not_created()
{
Assert.Throws<DomainException>(() => sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = ".*" })));
}
[Fact]
public void AddPattern_should_create_events()
{
CreateApp();
sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" }));
Assert.Single(sut.State.Patterns);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppPatternAdded { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
[Fact]
public void DeletePattern_should_throw_exception_if_app_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.DeletePattern(CreateCommand(new DeletePattern
{
PatternId = Guid.NewGuid()
}));
});
}
[Fact]
public void DeletePattern_should_create_events()
{
CreateApp();
CreatePattern();
sut.DeletePattern(CreateCommand(new DeletePattern { PatternId = patternId }));
Assert.Empty(sut.State.Patterns);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppPatternDeleted { PatternId = patternId })
);
}
[Fact]
public void UpdatePattern_should_throw_exception_if_app_not_created()
{
Assert.Throws<DomainException>(() => sut.UpdatePattern(CreateCommand(new UpdatePattern { PatternId = patternId, Name = "Any", Pattern = ".*" })));
}
[Fact]
public void UpdatePattern_should_create_events()
{
CreateApp();
CreatePattern();
sut.UpdatePattern(CreateCommand(new UpdatePattern { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" }));
Assert.Single(sut.State.Patterns);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppPatternUpdated { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
private void CreatePattern()
{
sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Name", Pattern = ".*" }));
sut.ClearUncommittedEvents();
}
private void CreateApp()
{
sut.Create(CreateCommand(new CreateApp { Name = AppName }));

169
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs

@ -0,0 +1,169 @@
// ==========================================================================
// GuardAppPatternsTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Guards;
using Squidex.Infrastructure;
using Xunit;
#pragma warning disable SA1310 // Field names must not contain underscore
namespace Squidex.Domain.Apps.Write.Apps.Guards
{
public class GuardAppPatternsTests
{
private readonly Guid patternId = Guid.NewGuid();
private readonly AppPatterns patterns_0 = AppPatterns.Empty;
[Fact]
public void CanAdd_should_throw_exception_if_name_empty()
{
var command = new AddPattern { PatternId = patternId, Name = string.Empty, Pattern = ".*" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanAdd(patterns_0, command));
}
[Fact]
public void CanAdd_should_throw_exception_if_id_empty_guid()
{
var command = new AddPattern { Name = string.Empty, Pattern = ".*" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanAdd(patterns_0, command));
}
[Fact]
public void CanAdd_should_throw_exception_if_pattern_empty()
{
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = string.Empty };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanAdd(patterns_0, command));
}
[Fact]
public void CanAdd_should_throw_exception_if_pattern_not_valid()
{
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = "[0-9{1}" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanAdd(patterns_0, command));
}
[Fact]
public void CanAdd_should_throw_exception_if_name_exists()
{
var patterns_1 = patterns_0.Add(Guid.NewGuid(), "any", "[a-z]", "Message");
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = ".*" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanAdd(patterns_1, command));
}
[Fact]
public void CanAdd_should_not_throw_exception_if_success()
{
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = ".*" };
GuardAppPattern.CanAdd(patterns_0, command);
}
[Fact]
public void CanDelete_should_throw_exception_if_pattern_not_found()
{
var command = new DeletePattern { PatternId = patternId };
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppPattern.CanDelete(patterns_0, command));
}
[Fact]
public void CanDelete_should_not_throw_exception_if_success()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new DeletePattern { PatternId = patternId };
GuardAppPattern.CanDelete(patterns_1, command);
}
[Fact]
public void CanUpdate_should_throw_exception_if_name_empty()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = string.Empty, Pattern = ".*" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanUpdate(patterns_1, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_empty()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = "any", Pattern = string.Empty };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanUpdate(patterns_1, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_not_valid()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = "any", Pattern = "[0-9{1}" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanUpdate(patterns_1, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_name_exists()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var patterns_1 = patterns_0.Add(id1, "Pattern1", "[0-5]", "Message");
var patterns_2 = patterns_1.Add(id2, "Pattern2", "[0-4]", "Message");
var command = new UpdatePattern { PatternId = id2, Name = "Pattern1", Pattern = "[0-4]" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanUpdate(patterns_2, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_exists()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var patterns_1 = patterns_0.Add(id1, "Pattern1", "[0-5]", "Message");
var patterns_2 = patterns_1.Add(id2, "Pattern2", "[0-4]", "Message");
var command = new UpdatePattern { PatternId = id2, Name = "Pattern2", Pattern = "[0-5]" };
Assert.Throws<ValidationException>(() => GuardAppPattern.CanUpdate(patterns_2, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_does_not_exists()
{
var command = new UpdatePattern { PatternId = patternId, Name = "Pattern1", Pattern = ".*" };
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppPattern.CanUpdate(patterns_0, command));
}
[Fact]
public void CanUpdate_should_not_throw_exception_if_pattern_exist_with_valid_command()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = "Pattern1", Pattern = ".*" };
GuardAppPattern.CanUpdate(patterns_1, command);
}
}
}

43
tools/Migrate_01/Migration01.cs → tools/Migrate_01/Migration01_FromCqrs.cs

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Schemas;
@ -21,24 +20,21 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
namespace Migrate_01
{
public sealed class Migration01 : IMigration, IEventSubscriber
public sealed class Migration01_FromCqrs : IMigration
{
private readonly FieldRegistry fieldRegistry;
private readonly IEventStore eventStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly IStateFactory stateFactory;
private readonly Timer timer;
private readonly TaskCompletionSource<object> subscriptionTcs = new TaskCompletionSource<object>();
public int FromVersion { get; } = 0;
public int ToVersion { get; } = 1;
public Migration01(
public Migration01_FromCqrs(
FieldRegistry fieldRegistry,
IEventDataFormatter eventDataFormatter,
IEventStore eventStore,
@ -48,30 +44,12 @@ namespace Migrate_01
this.eventDataFormatter = eventDataFormatter;
this.eventStore = eventStore;
this.stateFactory = stateFactory;
timer = new Timer(d => subscriptionTcs.TrySetResult(true));
}
public async Task UpdateAsync()
{
var subscription = eventStore.CreateSubscription(this, ".*");
try
{
await subscriptionTcs.Task;
}
finally
{
await subscription.StopAsync();
}
}
public async Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent)
{
try
await eventStore.GetEventsAsync(async storedEvent =>
{
timer.Change(Timeout.Infinite, Timeout.Infinite);
var @event = ParseKnownEvent(storedEvent);
if (@event != null)
@ -111,20 +89,7 @@ namespace Migrate_01
await app.WriteStateAsync(version);
}
}
timer.Change(5000, 0);
}
catch (Exception ex)
{
subscriptionTcs.SetException(ex);
}
}
public Task OnErrorAsync(IEventSubscription subscription, Exception exception)
{
subscriptionTcs.TrySetException(exception);
return TaskHelper.Done;
}, CancellationToken.None);
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)

71
tools/Migrate_01/Migration02_AddPatterns.cs

@ -0,0 +1,71 @@
// ==========================================================================
// Migration02_AddPatterns.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
namespace Migrate_01
{
public sealed class Migration02_AddPatterns : IMigration
{
private readonly InitialPatterns initialPatterns;
private readonly IStateFactory stateFactory;
private readonly IAppRepository appRepository;
public int FromVersion { get; } = 1;
public int ToVersion { get; } = 2;
public Migration02_AddPatterns(
InitialPatterns initialPatterns,
IStateFactory stateFactory,
IAppRepository appRepository)
{
this.initialPatterns = initialPatterns;
this.appRepository = appRepository;
this.stateFactory = stateFactory;
}
public async Task UpdateAsync()
{
var ids = await appRepository.QueryAppIdsAsync();
foreach (var id in ids)
{
var app = await stateFactory.CreateAsync<AppDomainObject>(id);
if (app.State.Patterns.Count == 0)
{
foreach (var pattern in initialPatterns.Values)
{
var command =
new AddPattern
{
Actor = app.State.CreatedBy,
AppId = new NamedId<Guid>(app.State.Id, app.State.Name),
Name = pattern.Name,
PatternId = Guid.NewGuid(),
Pattern = pattern.Pattern,
Message = pattern.Message
};
app.AddPattern(command);
}
await app.WriteStateAsync(app.Version + initialPatterns.Count);
}
}
}
}
}
Loading…
Cancel
Save