From 24428e80821922780bedc0aa2a2d6b56fe8d7385 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 13 Jan 2018 23:51:58 +0100 Subject: [PATCH 01/26] Started filter support. --- .../Rules/MongoRuleEventRepository.cs | 4 +- .../Repositories/IRuleEventRepository.cs | 2 +- .../Rules/RuleDequeuer.cs | 4 +- .../EventSourcing/Formatter.cs | 12 +- .../EventSourcing/GetEventStore.cs | 66 +++-- .../GetEventStoreSubscription.cs | 27 +-- .../EventSourcing/ProjectionClient.cs | 155 ++++++++++++ .../EventSourcing/ProjectionHelper.cs | 97 -------- .../EventSourcing/MongoEvent.cs | 9 +- .../EventSourcing/MongoEventStore.cs | 228 +----------------- .../EventSourcing/MongoEventStore_Reader.cs | 173 +++++++++++++ .../EventSourcing/MongoEventStore_Writer.cs | 135 +++++++++++ .../MongoDb/BsonJsonConvention.cs | 13 + .../MongoDb/JTokenSerializer.cs | 32 +++ ...matter.cs => DefaultEventDataFormatter.cs} | 39 ++- .../EventSourcing/EventData.cs | 8 +- .../EventSourcing/IEventStore.cs | 12 +- .../EventSourcing/PollingSubscription.cs | 2 +- .../EventSourcing/StoredEvent.cs | 26 +- .../Persistence{TOwner,TSnapshot,TKey}.cs | 4 +- .../Config/Domain/InfrastructureServices.cs | 2 +- .../EventSourcing/EventDataFormatterTests.cs | 4 +- .../EventSourcing/PollingSubscriptionTests.cs | 12 +- .../States/PersistenceEventSourcingTests.cs | 14 +- .../Migration00_ConvertEventStore.cs | 53 ++++ tools/Migrate_01/Rebuilder.cs | 12 +- 26 files changed, 679 insertions(+), 466 deletions(-) create mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs delete mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs create mode 100644 src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs create mode 100644 src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs create mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs rename src/Squidex.Infrastructure/EventSourcing/{JsonEventDataFormatter.cs => DefaultEventDataFormatter.cs} (51%) create mode 100644 tools/Migrate_01/Migration00_ConvertEventStore.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs index d54da9c2d..468d8d831 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -39,9 +39,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }); } - public Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)) + public Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default(CancellationToken)) { - return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, cancellationToken); + return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); } public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs index 8edc62f37..31c09131b 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); - Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)); + Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default(CancellationToken)); Task CountByAppAsync(Guid appId); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs index 8150f7013..6c0926902 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs @@ -71,13 +71,13 @@ namespace Squidex.Domain.Apps.Entities.Rules timer.SkipCurrentDelay(); } - private async Task QueryAsync(CancellationToken cancellationToken) + private async Task QueryAsync(CancellationToken ct) { try { var now = clock.GetCurrentInstant(); - await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, cancellationToken); + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, ct); } catch (Exception ex) { diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs index f0721a586..aefd883b2 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Text; using EventStore.ClientAPI; using EventStoreData = EventStore.ClientAPI.EventData; @@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing var body = Encoding.UTF8.GetString(@event.Data); var meta = Encoding.UTF8.GetString(@event.Metadata); - var eventData = new EventData { Type = @event.EventType, EventId = @event.EventId, Payload = body, Metadata = meta }; + var eventData = new EventData { Type = @event.EventType, Payload = body, Metadata = meta }; return new StoredEvent( resolvedEvent.OriginalEventNumber.ToString(), @@ -30,13 +31,10 @@ namespace Squidex.Infrastructure.EventSourcing public static EventStoreData Write(EventData eventData) { - var body = Encoding.UTF8.GetBytes(eventData.Payload); - var meta = Encoding.UTF8.GetBytes(eventData.Metadata); + var body = Encoding.UTF8.GetBytes(eventData.Payload.ToString()); + var meta = Encoding.UTF8.GetBytes(eventData.Metadata.ToString()); - return new EventStoreData( - eventData.EventId, - eventData.Type, - true, body, meta); + return new EventStoreData(Guid.NewGuid(), eventData.Type, true, body, meta); } } } diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index 2023d0f73..9e4d1a4d8 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -11,7 +11,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using EventStore.ClientAPI; -using EventStore.ClientAPI.Projections; namespace Squidex.Infrastructure.EventSourcing { @@ -20,18 +19,18 @@ namespace Squidex.Infrastructure.EventSourcing private const int WritePageSize = 500; private const int ReadPageSize = 500; private readonly IEventStoreConnection connection; - private readonly string projectionHost; private readonly string prefix; - private ProjectionsManager projectionsManager; + private ProjectionClient projectionClient; public GetEventStore(IEventStoreConnection connection, string prefix, string projectionHost) { Guard.NotNull(connection, nameof(connection)); this.connection = connection; - this.projectionHost = projectionHost; this.prefix = prefix?.Trim(' ', '-').WithFallback("squidex"); + + projectionClient = new ProjectionClient(connection, prefix, projectionHost); } public void Initialize() @@ -45,50 +44,43 @@ namespace Squidex.Infrastructure.EventSourcing throw new ConfigurationException("Cannot connect to event store.", ex); } - try - { - projectionsManager = connection.GetProjectionsManagerAsync(projectionHost).Result; - - projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials).Wait(); - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex); - } + projectionClient.ConnectAsync().Wait(); } public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null) { - return new GetEventStoreSubscription(connection, subscriber, projectionsManager, prefix, position, streamFilter); + return new GetEventStoreSubscription(connection, subscriber, projectionClient, prefix, position, streamFilter); } - public async Task GetEventsAsync(Func callback, string streamFilter = null, string position = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateIndexAsync(string property) { - var streamName = await connection.CreateProjectionAsync(projectionsManager, prefix, streamFilter); + return projectionClient.CreateProjectionAsync(property, string.Empty); + } - var sliceStart = ProjectionHelper.ParsePosition(position); + public async Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default(CancellationToken)) + { + var streamName = await projectionClient.CreateProjectionAsync(property, value); - StreamEventsSlice currentSlice; - do - { - currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true); + var sliceStart = projectionClient.ParsePosition(position); - if (currentSlice.Status == SliceReadStatus.Success) - { - sliceStart = currentSlice.NextEventNumber; + await QueryAsync(callback, streamName, sliceStart, ct); + } - foreach (var resolved in currentSlice.Events) - { - var storedEvent = Formatter.Read(resolved); + public async Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default(CancellationToken)) + { + var streamName = await projectionClient.CreateProjectionAsync(streamFilter); - await callback(storedEvent); - } - } - } - while (!currentSlice.IsEndOfStream && !cancellationToken.IsCancellationRequested); + var sliceStart = projectionClient.ParsePosition(position); + + await QueryAsync(callback, streamName, sliceStart, ct); + } + + private Task QueryAsync(Func callback, string streamName, long sliceStart, CancellationToken ct) + { + return QueryAsync(callback, GetStreamName(streamName), sliceStart, ct); } - public async Task> GetEventsAsync(string streamName, long streamPosition = 0) + public async Task> QueryAsync(string streamName, long streamPosition = 0) { var result = new List(); @@ -97,7 +89,7 @@ namespace Squidex.Infrastructure.EventSourcing StreamEventsSlice currentSlice; do { - currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, false); + currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false); if (currentSlice.Status == SliceReadStatus.Success) { @@ -116,12 +108,12 @@ namespace Squidex.Infrastructure.EventSourcing return result; } - public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) + public Task AppendAsync(Guid commitId, string streamName, ICollection events) { return AppendEventsInternalAsync(streamName, EtagVersion.Any, events); } - public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) { Guard.GreaterEquals(expectedVersion, -1, nameof(expectedVersion)); diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index 3fbffbe37..cbf1559f5 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -8,33 +8,32 @@ using System.Threading.Tasks; using EventStore.ClientAPI; using EventStore.ClientAPI.Exceptions; -using EventStore.ClientAPI.Projections; using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.EventSourcing { internal sealed class GetEventStoreSubscription : IEventSubscription { - private readonly IEventStoreConnection eventStoreConnection; - private readonly IEventSubscriber eventSubscriber; + private readonly IEventStoreConnection connection; + private readonly IEventSubscriber subscriber; private readonly EventStoreCatchUpSubscription subscription; private readonly long? position; public GetEventStoreSubscription( - IEventStoreConnection eventStoreConnection, - IEventSubscriber eventSubscriber, - ProjectionsManager projectionsManager, + IEventStoreConnection connection, + IEventSubscriber subscriber, + ProjectionClient projectionClient, string prefix, string position, string streamFilter) { - Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); + Guard.NotNull(subscriber, nameof(subscriber)); - this.eventStoreConnection = eventStoreConnection; - this.eventSubscriber = eventSubscriber; - this.position = ProjectionHelper.ParsePositionOrNull(position); + this.connection = connection; + this.position = projectionClient.ParsePositionOrNull(position); + this.subscriber = subscriber; - var streamName = eventStoreConnection.CreateProjectionAsync(projectionsManager, prefix, streamFilter).Result; + var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; subscription = SubscribeToStream(streamName); } @@ -50,12 +49,12 @@ namespace Squidex.Infrastructure.EventSourcing { var settings = CatchUpSubscriptionSettings.Default; - return eventStoreConnection.SubscribeToStreamFrom(streamName, position, settings, + return connection.SubscribeToStreamFrom(streamName, position, settings, (s, e) => { var storedEvent = Formatter.Read(e); - eventSubscriber.OnEventAsync(this, storedEvent).Wait(); + subscriber.OnEventAsync(this, storedEvent).Wait(); }, null, (s, reason, ex) => { @@ -64,7 +63,7 @@ namespace Squidex.Infrastructure.EventSourcing { ex = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}."); - eventSubscriber.OnErrorAsync(this, ex); + subscriber.OnErrorAsync(this, ex); } }); } diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs new file mode 100644 index 000000000..13dbc1558 --- /dev/null +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs @@ -0,0 +1,155 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using EventStore.ClientAPI.Projections; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class ProjectionClient + { + private const string StreamByFilter = "by-{0}-{1}"; + private const string StreamByProperty = "by-{0}-{1}-property"; + private readonly ConcurrentDictionary projections = new ConcurrentDictionary(); + private readonly IEventStoreConnection connection; + private readonly string prefix; + private readonly string projectionHost; + private ProjectionsManager projectionsManager; + + public ProjectionClient(IEventStoreConnection connection, string prefix, string projectionHost) + { + this.connection = connection; + + this.prefix = prefix; + this.projectionHost = projectionHost; + } + + private string CreateFilterStreamName(string filter) + { + return string.Format(CultureInfo.InvariantCulture, StreamByFilter, prefix.Simplify(), filter.Simplify()); + } + + private string CreatePropertyStreamName(string property) + { + return string.Format(CultureInfo.InvariantCulture, StreamByFilter, prefix.Simplify(), property.Simplify()); + } + + public async Task CreateProjectionAsync(string property, object value) + { + var streamName = CreatePropertyStreamName(property); + + if (projections.TryAdd(streamName, true)) + { + var projectionConfig = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && e.data.{property}) {{ + linkTo('{streamName}-' + e.data.{property}, e); + }} + }} + }});"; + + try + { + var credentials = connection.Settings.DefaultUserCredentials; + + await projectionsManager.CreateContinuousAsync($"{streamName}", projectionConfig, credentials); + } + catch (Exception ex) + { + if (!ex.Is()) + { + throw; + } + } + } + + return streamName + "-" + value; + } + + public async Task CreateProjectionAsync(string streamFilter = null) + { + streamFilter = streamFilter ?? ".*"; + + var streamName = CreateFilterStreamName(streamFilter); + + if (projections.TryAdd(streamName, true)) + { + var projectionConfig = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ + linkTo('{streamName}', e); + }} + }} + }});"; + + try + { + var credentials = connection.Settings.DefaultUserCredentials; + + await projectionsManager.CreateContinuousAsync($"{streamName}", projectionConfig, credentials); + } + catch (Exception ex) + { + if (!ex.Is()) + { + throw; + } + } + } + + return streamName; + } + + public async Task ConnectAsync() + { + var addressParts = projectionHost.Split(':'); + + if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port)) + { + port = 2113; + } + + var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]); + var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port); + + projectionsManager = + new ProjectionsManager( + connection.Settings.Log, endpoint, + connection.Settings.OperationTimeout); + try + { + await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials); + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex); + } + } + + public long? ParsePositionOrNull(string position) + { + return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; + } + + public long ParsePosition(string position) + { + return long.TryParse(position, out var parsedPosition) ? parsedPosition : 0; + } + } +} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs deleted file mode 100644 index 3baccf1a2..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; -using EventStore.ClientAPI.Projections; - -namespace Squidex.Infrastructure.EventSourcing -{ - public static class ProjectionHelper - { - private const string ProjectionName = "by-{0}-{1}"; - private static readonly ConcurrentDictionary SubscriptionsCreated = new ConcurrentDictionary(); - - private static string ParseFilter(string prefix, string filter) - { - return string.Format(CultureInfo.InvariantCulture, ProjectionName, prefix.Simplify(), filter.Simplify()); - } - - public static async Task CreateProjectionAsync(this IEventStoreConnection connection, ProjectionsManager projectionsManager, string prefix, string streamFilter = null) - { - streamFilter = streamFilter ?? ".*"; - - var streamName = ParseFilter(prefix, streamFilter); - - if (SubscriptionsCreated.TryAdd(streamName, true)) - { - var projectionConfig = - $@"fromAll() - .when({{ - $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ - linkTo('{streamName}', e); - }} - }} - }});"; - - try - { - var credentials = connection.Settings.DefaultUserCredentials; - - await projectionsManager.CreateContinuousAsync($"${streamName}", projectionConfig, credentials); - } - catch (Exception ex) - { - if (!ex.Is()) - { - throw; - } - } - } - - return streamName; - } - - public static async Task GetProjectionsManagerAsync(this IEventStoreConnection connection, string projectionHost) - { - var addressParts = projectionHost.Split(':'); - - if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port)) - { - port = 2113; - } - - var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]); - var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port); - - var projectionsManager = - new ProjectionsManager( - connection.Settings.Log, endpoint, - connection.Settings.OperationTimeout); - - return projectionsManager; - } - - public static long? ParsePositionOrNull(string position) - { - return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; - } - - public static long ParsePosition(string position) - { - return long.TryParse(position, out var parsedPosition) ? parsedPosition : 0; - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs index 703fb5223..81649a439 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs @@ -7,6 +7,7 @@ using System; using MongoDB.Bson.Serialization.Attributes; +using Newtonsoft.Json.Linq; using Squidex.Infrastructure.Reflection; namespace Squidex.Infrastructure.EventSourcing @@ -15,15 +16,11 @@ namespace Squidex.Infrastructure.EventSourcing { [BsonElement] [BsonRequired] - public Guid EventId { get; set; } + public JToken Payload { get; set; } [BsonElement] [BsonRequired] - public string Payload { get; set; } - - [BsonElement] - [BsonRequired] - public string Metadata { get; set; } + public JToken Metadata { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 2b4c072a3..e5779e42e 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -5,10 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; @@ -16,16 +12,19 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.EventSourcing { - public class MongoEventStore : MongoRepositoryBase, IEventStore + public partial class MongoEventStore : MongoRepositoryBase, IEventStore { - private const int MaxAttempts = 20; - private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); private static readonly FieldDefinition EventsCountField = Fields.Build(x => x.EventsCount); private static readonly FieldDefinition EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); private static readonly FieldDefinition EventStreamField = Fields.Build(x => x.EventStream); private readonly IEventNotifier notifier; + public IMongoCollection RawCollection + { + get { return Database.GetCollection(CollectionName()); } + } + public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) : base(database) { @@ -50,220 +49,5 @@ namespace Squidex.Infrastructure.EventSourcing collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Timestamp).Ascending(x => x.EventStream)), collection.Indexes.CreateOneAsync(Index.Ascending(x => x.EventStream).Descending(x => x.EventStreamOffset), new CreateIndexOptions { Unique = true })); } - - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null) - { - Guard.NotNull(subscriber, nameof(subscriber)); - Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter)); - - return new PollingSubscription(this, notifier, subscriber, streamFilter, position); - } - - public async Task> GetEventsAsync(string streamName, long streamPosition = 0) - { - var commits = - await Collection.Find( - Filter.And( - Filter.Eq(EventStreamField, streamName), - Filter.Gte(EventStreamOffsetField, streamPosition - 1))) - .Sort(Sort.Ascending(TimestampField)).ToListAsync(); - - var result = new List(); - - foreach (var commit in commits) - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var e in commit.Events) - { - eventStreamOffset++; - - if (eventStreamOffset >= streamPosition) - { - var eventData = e.ToEventData(); - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - result.Add(new StoredEvent(eventToken, eventStreamOffset, eventData)); - } - } - } - - return result; - } - - public async Task GetEventsAsync(Func callback, string streamFilter = null, string position = null, CancellationToken cancellationToken = default(CancellationToken)) - { - Guard.NotNull(callback, nameof(callback)); - - StreamPosition lastPosition = position; - - var filter = CreateFilter(streamFilter, lastPosition); - - await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachAsync(async commit => - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var e in commit.Events) - { - eventStreamOffset++; - - if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) - { - var eventData = e.ToEventData(); - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - await callback(new StoredEvent(eventToken, eventStreamOffset, eventData)); - - commitOffset++; - } - } - }, cancellationToken); - } - - public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) - { - return AppendEventsInternalAsync(commitId, streamName, EtagVersion.Any, events); - } - - public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); - - return AppendEventsInternalAsync(commitId, streamName, expectedVersion, events); - } - - private async Task AppendEventsInternalAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - - if (events.Count == 0) - { - return; - } - - var currentVersion = await GetEventStreamOffset(streamName); - - if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); - - for (var attempt = 0; attempt < MaxAttempts; attempt++) - { - try - { - await Collection.InsertOneAsync(commit); - - notifier.NotifyEventsStored(streamName); - - return; - } - catch (MongoWriteException ex) - { - if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) - { - currentVersion = await GetEventStreamOffset(streamName); - - if (expectedVersion != EtagVersion.Any) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - else if (attempt < MaxAttempts) - { - expectedVersion = currentVersion; - } - else - { - throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); - } - } - else - { - throw; - } - } - } - } - - private async Task GetEventStreamOffset(string streamName) - { - var document = - await Collection.Find(Filter.Eq(EventStreamField, streamName)) - .Project(Projection - .Include(EventStreamOffsetField) - .Include(EventsCountField)) - .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) - .FirstOrDefaultAsync(); - - if (document != null) - { - return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); - } - - return EtagVersion.Empty; - } - - private static FilterDefinition CreateFilter(string streamFilter, StreamPosition streamPosition) - { - var filters = new List>(); - - if (streamPosition.IsEndOfCommit) - { - filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp)); - } - else - { - filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp)); - } - - if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, ".*", StringComparison.OrdinalIgnoreCase)) - { - if (streamFilter.Contains("^")) - { - filters.Add(Filter.Regex(EventStreamField, streamFilter)); - } - else - { - filters.Add(Filter.Eq(EventStreamField, streamFilter)); - } - } - - return Filter.And(filters); - } - - private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - var commitEvents = new MongoEvent[events.Count]; - - var i = 0; - - foreach (var e in events) - { - var mongoEvent = new MongoEvent(e); - - commitEvents[i++] = mongoEvent; - } - - var mongoCommit = new MongoEventCommit - { - Id = commitId, - Events = commitEvents, - EventsCount = events.Count, - EventStream = streamName, - EventStreamOffset = expectedVersion, - Timestamp = EmptyTimestamp - }; - - return mongoCommit; - } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs new file mode 100644 index 000000000..ccc7737e9 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -0,0 +1,173 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class MongoEventStore : MongoRepositoryBase, IEventStore + { + public Task CreateIndexAsync(string property) + { + return Collection.Indexes.CreateOneAsync(Index.Ascending(CreateIndexPath(property))); + } + + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null) + { + Guard.NotNull(subscriber, nameof(subscriber)); + Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter)); + + return new PollingSubscription(this, notifier, subscriber, streamFilter, position); + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + var commits = + await Collection.Find( + Filter.And( + Filter.Eq(EventStreamField, streamName), + Filter.Gte(EventStreamOffsetField, streamPosition - 1))) + .Sort(Sort.Ascending(TimestampField)).ToListAsync(); + + var result = new List(); + + foreach (var commit in commits) + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var e in commit.Events) + { + eventStreamOffset++; + + if (eventStreamOffset >= streamPosition) + { + var eventData = e.ToEventData(); + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + result.Add(new StoredEvent(eventToken, eventStreamOffset, eventData)); + } + } + } + + return result; + } + + public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default(CancellationToken)) + { + Guard.NotNull(callback, nameof(callback)); + + StreamPosition lastPosition = position; + + var filter = CreateFilter(property, value, lastPosition); + + return QueryAsync(callback, lastPosition, filter, ct); + } + + public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default(CancellationToken)) + { + Guard.NotNull(callback, nameof(callback)); + + StreamPosition lastPosition = position; + + var filter = CreateFilter(streamFilter, lastPosition); + + return QueryAsync(callback, lastPosition, filter, ct); + } + + private async Task QueryAsync(Func callback, StreamPosition lastPosition, FilterDefinition filter, CancellationToken ct) + { + await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachAsync(async commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var e in commit.Events) + { + eventStreamOffset++; + + if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) + { + var eventData = e.ToEventData(); + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + await callback(new StoredEvent(eventToken, eventStreamOffset, eventData)); + + commitOffset++; + } + } + }, ct); + } + + private static FilterDefinition CreateFilter(string property, object value, StreamPosition streamPosition) + { + var filters = new List>(); + + AddPositionFilter(streamPosition, filters); + AddPropertyFitler(property, value, filters); + + return Filter.And(filters); + } + + private static FilterDefinition CreateFilter(string streamFilter, StreamPosition streamPosition) + { + var filters = new List>(); + + AddPositionFilter(streamPosition, filters); + AddStreamFilter(streamFilter, filters); + + return Filter.And(filters); + } + + private static void AddPropertyFitler(string property, object value, List> filters) + { + filters.Add(Filter.Eq(CreateIndexPath(property), value)); + } + + private static void AddStreamFilter(string streamFilter, List> filters) + { + if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, ".*", StringComparison.OrdinalIgnoreCase)) + { + if (streamFilter.Contains("^")) + { + filters.Add(Filter.Regex(EventStreamField, streamFilter)); + } + else + { + filters.Add(Filter.Eq(EventStreamField, streamFilter)); + } + } + } + + private static void AddPositionFilter(StreamPosition streamPosition, List> filters) + { + if (streamPosition.IsEndOfCommit) + { + filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp)); + } + else + { + filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp)); + } + } + + private static string CreateIndexPath(string property) + { + return $"Events.Payload.{property}"; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs new file mode 100644 index 000000000..9c0e3f877 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -0,0 +1,135 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class MongoEventStore + { + private const int MaxWriteAttempts = 20; + private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendEventsInternalAsync(commitId, streamName, EtagVersion.Any, events); + } + + public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); + + return AppendEventsInternalAsync(commitId, streamName, expectedVersion, events); + } + + private async Task AppendEventsInternalAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + Guard.NotNull(events, nameof(events)); + + if (events.Count == 0) + { + return; + } + + var currentVersion = await GetEventStreamOffset(streamName); + + if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); + + for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) + { + try + { + await Collection.InsertOneAsync(commit); + + notifier.NotifyEventsStored(streamName); + + return; + } + catch (MongoWriteException ex) + { + if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + { + currentVersion = await GetEventStreamOffset(streamName); + + if (expectedVersion != EtagVersion.Any) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + if (attempt < MaxWriteAttempts) + { + expectedVersion = currentVersion; + } + else + { + throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); + } + } + else + { + throw; + } + } + } + } + + private async Task GetEventStreamOffset(string streamName) + { + var document = + await Collection.Find(Filter.Eq(EventStreamField, streamName)) + .Project(Projection + .Include(EventStreamOffsetField) + .Include(EventsCountField)) + .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) + .FirstOrDefaultAsync(); + + if (document != null) + { + return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); + } + + return EtagVersion.Empty; + } + + private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + var commitEvents = new MongoEvent[events.Count]; + + var i = 0; + + foreach (var e in events) + { + var mongoEvent = new MongoEvent(e); + + commitEvents[i++] = mongoEvent; + } + + var mongoCommit = new MongoEventCommit + { + Id = commitId, + Events = commitEvents, + EventsCount = events.Count, + EventStream = streamName, + EventStreamOffset = expectedVersion, + Timestamp = EmptyTimestamp + }; + + return mongoCommit; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index ab9f13d11..2a8d6e572 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -11,6 +11,7 @@ using System.Reflection; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Squidex.Infrastructure.MongoDb { @@ -31,6 +32,18 @@ namespace Squidex.Infrastructure.MongoDb memberMap.SetSerializer((IBsonSerializer)bsonSerializer); } + else if (memberMap.MemberType == typeof(JToken)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JObject)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JValue)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } }); ConventionRegistry.Register("json", pack, t => true); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs new file mode 100644 index 000000000..fbb1039e0 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class JTokenSerializer : ClassSerializerBase where T : JToken + { + public static readonly JTokenSerializer Instance = new JTokenSerializer(); + + protected override T DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var jsonReader = new BsonJsonReader(context.Reader); + + return (T)JToken.ReadFrom(jsonReader); + } + + protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, T value) + { + var jsonWriter = new BsonJsonWriter(context.Writer); + + value.WriteTo(jsonWriter); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/JsonEventDataFormatter.cs b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs similarity index 51% rename from src/Squidex.Infrastructure/EventSourcing/JsonEventDataFormatter.cs rename to src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs index 9af9e3a68..83ce9ed5a 100644 --- a/src/Squidex.Infrastructure/EventSourcing/JsonEventDataFormatter.cs +++ b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs @@ -7,38 +7,37 @@ using System; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Squidex.Infrastructure.EventSourcing { - public class JsonEventDataFormatter : IEventDataFormatter + public class DefaultEventDataFormatter : IEventDataFormatter { - private readonly JsonSerializerSettings serializerSettings; + private readonly JsonSerializer serializer; private readonly TypeNameRegistry typeNameRegistry; - public JsonEventDataFormatter(TypeNameRegistry typeNameRegistry, JsonSerializerSettings serializerSettings = null) + public DefaultEventDataFormatter(TypeNameRegistry typeNameRegistry, JsonSerializer serializer = null) { Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); this.typeNameRegistry = typeNameRegistry; - this.serializerSettings = serializerSettings ?? new JsonSerializerSettings(); + this.serializer = serializer ?? JsonSerializer.CreateDefault(); } public Envelope Parse(EventData eventData, bool migrate = true) { - var headers = ReadJson(eventData.Metadata); - var eventType = typeNameRegistry.GetType(eventData.Type); - var eventPayload = ReadJson(eventData.Payload, eventType); - if (migrate && eventPayload is IMigratedEvent migratedEvent) + var headers = eventData.Metadata.ToObject(); + var content = eventData.Metadata.ToObject(eventType, serializer) as IEvent; + + if (migrate && content is IMigratedEvent migratedEvent) { - eventPayload = migratedEvent.Migrate(); + content = migratedEvent.Migrate(); } - var envelope = new Envelope(eventPayload, headers); - - envelope.SetEventId(eventData.EventId); + var envelope = new Envelope(content, headers); return envelope; } @@ -56,20 +55,10 @@ namespace Squidex.Infrastructure.EventSourcing envelope.SetCommitId(commitId); - var headers = WriteJson(envelope.Headers); - var content = WriteJson(envelope.Payload); + var headers = JToken.FromObject(envelope.Headers, serializer); + var content = JToken.FromObject(envelope.Payload, serializer); - return new EventData { EventId = envelope.Headers.EventId(), Type = eventType, Payload = content, Metadata = headers }; - } - - private T ReadJson(string data, Type type = null) - { - return (T)JsonConvert.DeserializeObject(data, type ?? typeof(T), serializerSettings); - } - - private string WriteJson(object value) - { - return JsonConvert.SerializeObject(value, serializerSettings); + return new EventData { Type = eventType, Payload = content, Metadata = headers }; } } } diff --git a/src/Squidex.Infrastructure/EventSourcing/EventData.cs b/src/Squidex.Infrastructure/EventSourcing/EventData.cs index 9ca13635d..739ea8068 100644 --- a/src/Squidex.Infrastructure/EventSourcing/EventData.cs +++ b/src/Squidex.Infrastructure/EventSourcing/EventData.cs @@ -5,17 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; +using Newtonsoft.Json.Linq; namespace Squidex.Infrastructure.EventSourcing { public class EventData { - public Guid EventId { get; set; } + public JToken Payload { get; set; } - public string Payload { get; set; } - - public string Metadata { get; set; } + public JToken Metadata { get; set; } public string Type { get; set; } } diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs index 2993ef86b..c33d86e5a 100644 --- a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs +++ b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs @@ -14,13 +14,17 @@ namespace Squidex.Infrastructure.EventSourcing { public interface IEventStore { - Task> GetEventsAsync(string streamName, long streamPosition = 0); + Task CreateIndexAsync(string property); - Task GetEventsAsync(Func callback, string streamFilter = null, string position = null, CancellationToken cancellationToken = default(CancellationToken)); + Task> QueryAsync(string streamName, long streamPosition = 0); - Task AppendEventsAsync(Guid commitId, string streamName, ICollection events); + Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default(CancellationToken)); - Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events); + Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default(CancellationToken)); + + Task AppendAsync(Guid commitId, string streamName, ICollection events); + + Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events); IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null); } diff --git a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs index dd5fc072b..7cbb556b9 100644 --- a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs +++ b/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs @@ -46,7 +46,7 @@ namespace Squidex.Infrastructure.EventSourcing { try { - await eventStore.GetEventsAsync(async storedEvent => + await eventStore.QueryAsync(async storedEvent => { await eventSubscriber.OnEventAsync(this, storedEvent); diff --git a/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs b/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs index 747dce5bc..3c93e21a4 100644 --- a/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs +++ b/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs @@ -9,33 +9,21 @@ namespace Squidex.Infrastructure.EventSourcing { public sealed class StoredEvent { - private readonly string eventPosition; - private readonly long eventStreamNumber; - private readonly EventData data; + public string EventPosition { get; } - public string EventPosition - { - get { return eventPosition; } - } - - public long EventStreamNumber - { - get { return eventStreamNumber; } - } + public long EventStreamNumber { get; } - public EventData Data - { - get { return data; } - } + public EventData Data { get; } public StoredEvent(string eventPosition, long eventStreamNumber, EventData data) { Guard.NotNullOrEmpty(eventPosition, nameof(eventPosition)); Guard.NotNull(data, nameof(data)); - this.data = data; - this.eventPosition = eventPosition; - this.eventStreamNumber = eventStreamNumber; + Data = data; + + EventPosition = eventPosition; + EventStreamNumber = eventStreamNumber; } } } diff --git a/src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs index 024a05119..cb0900aed 100644 --- a/src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs +++ b/src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs @@ -101,7 +101,7 @@ namespace Squidex.Infrastructure.States { if (UseEventSourcing()) { - var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1); + var events = await eventStore.QueryAsync(GetStreamName(), versionEvents + 1); foreach (var @event in events) { @@ -160,7 +160,7 @@ namespace Squidex.Infrastructure.States try { - await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); + await eventStore.AppendAsync(commitId, GetStreamName(), expectedVersion, eventData); } catch (WrongEventVersionException ex) { diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 6781b36e7..9a182bf87 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -89,7 +89,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs() .As(); services.AddSingletonAs() diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs index c0272f796..6d9406d95 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Infrastructure.EventSourcing private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); - private readonly JsonEventDataFormatter sut; + private readonly DefaultEventDataFormatter sut; public EventDataFormatterTests() { @@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing typeNameRegistry.Map(typeof(MyEvent), "Event"); typeNameRegistry.Map(typeof(MyOldEvent), "OldEvent"); - sut = new JsonEventDataFormatter(typeNameRegistry, serializerSettings); + sut = new DefaultEventDataFormatter(typeNameRegistry, JsonSerializer.Create(serializerSettings)); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs index 4c58d2655..19baac3de 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.EventSourcing await WaitAndStopAsync(sut); - A.CallTo(() => eventStore.GetEventsAsync(A>.Ignored, "^my-stream", position, A.Ignored)) + A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -36,7 +36,7 @@ namespace Squidex.Infrastructure.EventSourcing { var ex = new InvalidOperationException(); - A.CallTo(() => eventStore.GetEventsAsync(A>.Ignored, "^my-stream", position, A.Ignored)) + A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .Throws(ex); var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); @@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.EventSourcing { var ex = new OperationCanceledException(); - A.CallTo(() => eventStore.GetEventsAsync(A>.Ignored, "^my-stream", position, A.Ignored)) + A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .Throws(ex); var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); @@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.EventSourcing { var ex = new AggregateException(new OperationCanceledException()); - A.CallTo(() => eventStore.GetEventsAsync(A>.Ignored, "^my-stream", position, A.Ignored)) + A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .Throws(ex); var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); @@ -88,7 +88,7 @@ namespace Squidex.Infrastructure.EventSourcing await WaitAndStopAsync(sut); - A.CallTo(() => eventStore.GetEventsAsync(A>.Ignored, "^my-stream", position, A.Ignored)) + A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -101,7 +101,7 @@ namespace Squidex.Infrastructure.EventSourcing await WaitAndStopAsync(sut); - A.CallTo(() => eventStore.GetEventsAsync(A>.Ignored, "^my-stream", position, A.Ignored)) + A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .MustHaveHappened(Repeated.Exactly.Twice); } diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index 865d2d0b5..9eed300b9 100644 --- a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs @@ -118,7 +118,7 @@ namespace Squidex.Infrastructure.States await sut.GetSingleAsync(key); - A.CallTo(() => eventStore.GetEventsAsync(key, 3)) + A.CallTo(() => eventStore.QueryAsync(key, 3)) .MustHaveHappened(); } @@ -199,9 +199,9 @@ namespace Squidex.Infrastructure.States await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()); await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()); - A.CallTo(() => eventStore.AppendEventsAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .MustHaveHappened(); - A.CallTo(() => eventStore.AppendEventsAsync(A.Ignored, key, 4, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 4, A>.That.Matches(x => x.Count == 2))) .MustHaveHappened(); } @@ -212,7 +212,7 @@ namespace Squidex.Infrastructure.States var actualObject = await sut.GetSingleAsync(key); - A.CallTo(() => eventStore.AppendEventsAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .Throws(new WrongEventVersionException(1, 1)); await Assert.ThrowsAsync(() => statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent())); @@ -221,7 +221,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_remove_from_cache_when_write_failed() { - A.CallTo(() => eventStore.AppendEventsAsync(A.Ignored, A.Ignored, A.Ignored, A>.Ignored)) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, A.Ignored, A.Ignored, A>.Ignored)) .Throws(new InvalidOperationException()); var actualObject = await sut.GetSingleAsync(key); @@ -251,7 +251,7 @@ namespace Squidex.Infrastructure.States Assert.Same(retrievedStates[0], retrievedState); } - A.CallTo(() => eventStore.GetEventsAsync(key, 0)) + A.CallTo(() => eventStore.QueryAsync(key, 0)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -284,7 +284,7 @@ namespace Squidex.Infrastructure.States i++; } - A.CallTo(() => eventStore.GetEventsAsync(key, readPosition)) + A.CallTo(() => eventStore.QueryAsync(key, readPosition)) .Returns(eventsStored); } } diff --git a/tools/Migrate_01/Migration00_ConvertEventStore.cs b/tools/Migrate_01/Migration00_ConvertEventStore.cs new file mode 100644 index 000000000..4cac5ed83 --- /dev/null +++ b/tools/Migrate_01/Migration00_ConvertEventStore.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01 +{ + public sealed class Migration00_ConvertEventStore : IMigration + { + private readonly IEventStore eventStore; + + public int FromVersion { get; } = 0; + + public int ToVersion { get; } = 1; + + public Migration00_ConvertEventStore(IEventStore eventStore) + { + this.eventStore = eventStore; + } + + public async Task UpdateAsync(IEnumerable previousMigrations) + { + if (eventStore is MongoEventStore mongoEventStore) + { + var collection = mongoEventStore.RawCollection; + + var filter = Builders.Filter; + + await collection.Find(new BsonDocument()).ForEachAsync(async commit => + { + foreach (BsonDocument @event in commit["Events"].AsBsonArray) + { + @event.Remove("EventId"); + + @event["Payload"] = BsonDocument.Parse(@event["Payload"].AsString); + @event["Metadata"] = BsonDocument.Parse(@event["Metadata"].AsString); + } + + await collection.ReplaceOneAsync(filter.Eq("_id", commit["_id"].AsString), commit); + }); + } + } + } +} diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index a401c39d7..884664cdb 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -53,7 +53,7 @@ namespace Migrate_01 var handledIds = new HashSet(); - return eventStore.GetEventsAsync(async storedEvent => + return eventStore.QueryAsync(async storedEvent => { var @event = ParseKnownEvent(storedEvent); @@ -68,7 +68,7 @@ namespace Migrate_01 await asset.WriteSnapshotAsync(); } } - }, filter, cancellationToken: CancellationToken.None); + }, filter, ct: CancellationToken.None); } public Task RebuildConfigAsync() @@ -77,7 +77,7 @@ namespace Migrate_01 var handledIds = new HashSet(); - return eventStore.GetEventsAsync(async storedEvent => + return eventStore.QueryAsync(async storedEvent => { var @event = ParseKnownEvent(storedEvent); @@ -102,7 +102,7 @@ namespace Migrate_01 await app.WriteSnapshotAsync(); } } - }, filter, cancellationToken: CancellationToken.None); + }, filter, ct: CancellationToken.None); } public async Task RebuildContentAsync() @@ -113,7 +113,7 @@ namespace Migrate_01 await snapshotContentStore.ClearAsync(); - await eventStore.GetEventsAsync(async storedEvent => + await eventStore.QueryAsync(async storedEvent => { var @event = ParseKnownEvent(storedEvent); @@ -139,7 +139,7 @@ namespace Migrate_01 // Schema has been deleted. } } - }, filter, cancellationToken: CancellationToken.None); + }, filter, ct: CancellationToken.None); } private Envelope ParseKnownEvent(StoredEvent storedEvent) From 9743c5aa121fd0b1e7cb18d2500954bf31be7c77 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 13 Jan 2018 23:54:29 +0100 Subject: [PATCH 02/26] Code cleanup. --- .../EventSourcing/ProjectionClient.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs index 13dbc1558..6bf4f004d 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs @@ -20,8 +20,6 @@ namespace Squidex.Infrastructure.EventSourcing { public sealed class ProjectionClient { - private const string StreamByFilter = "by-{0}-{1}"; - private const string StreamByProperty = "by-{0}-{1}-property"; private readonly ConcurrentDictionary projections = new ConcurrentDictionary(); private readonly IEventStoreConnection connection; private readonly string prefix; @@ -38,12 +36,12 @@ namespace Squidex.Infrastructure.EventSourcing private string CreateFilterStreamName(string filter) { - return string.Format(CultureInfo.InvariantCulture, StreamByFilter, prefix.Simplify(), filter.Simplify()); + return $"by-{StreamByFilter}-{prefix.Simplify()}-{filter.Simplify()}"; } private string CreatePropertyStreamName(string property) { - return string.Format(CultureInfo.InvariantCulture, StreamByFilter, prefix.Simplify(), property.Simplify()); + return $"by-{StreamByFilter}-{prefix.Simplify()}-{property.Simplify()}-property"; } public async Task CreateProjectionAsync(string property, object value) @@ -77,7 +75,7 @@ namespace Squidex.Infrastructure.EventSourcing } } - return streamName + "-" + value; + return $"{streamName}-{value}"; } public async Task CreateProjectionAsync(string streamFilter = null) From 5ff426ae0daefd9829ef5037db2d2873bd238907 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 14 Jan 2018 22:03:18 +0100 Subject: [PATCH 03/26] Migrations improved --- .../SquidexCoreModel.cs | 3 + .../Contents/MongoContentRepository.cs | 1 - .../SquidexEvents.cs | 3 + .../EventSourcing/ProjectionClient.cs | 81 ++++++++----------- .../EventSourcing/MongoEvent.cs | 1 - .../EventSourcing/MongoEventStore_Writer.cs | 10 +-- .../MongoDb/MongoRepositoryBase.cs | 1 - .../DefaultEventDataFormatter.cs | 2 +- .../Migrations/IMigration.cs | 7 +- .../Migrations/IMigrationPath.cs | 16 ++++ .../Migrations/Migrator.cs | 58 ++++--------- .../SquidexInfrastructure.cs | 3 + .../Config/Domain/SerializationServices.cs | 8 +- src/Squidex/Config/Domain/WriteServices.cs | 13 ++- ...s.cs => DefaultEventDataFormatterTests.cs} | 4 +- .../Migrations/MigratorTests.cs | 79 ++++++------------ tools/Migrate_01/Migrate_01.csproj | 1 + tools/Migrate_01/MigrationPath.cs | 64 +++++++++++++++ .../AddPatterns.cs} | 13 +-- .../ConvertEventStore.cs} | 13 +-- .../RebuildContentCollections.cs} | 19 ++--- .../RebuildSnapshots.cs} | 13 +-- tools/Migrate_01/SquidexMigrations.cs | 16 ++++ 23 files changed, 219 insertions(+), 210 deletions(-) create mode 100644 src/Squidex.Infrastructure/Migrations/IMigrationPath.cs rename tests/Squidex.Infrastructure.Tests/EventSourcing/{EventDataFormatterTests.cs => DefaultEventDataFormatterTests.cs} (97%) create mode 100644 tools/Migrate_01/MigrationPath.cs rename tools/Migrate_01/{Migration02_AddPatterns.cs => Migrations/AddPatterns.cs} (83%) rename tools/Migrate_01/{Migration00_ConvertEventStore.cs => Migrations/ConvertEventStore.cs} (80%) rename tools/Migrate_01/{Migration03_SplitContentCollections.cs => Migrations/RebuildContentCollections.cs} (52%) rename tools/Migrate_01/{Migration01_FromCqrs.cs => Migrations/RebuildSnapshots.cs} (68%) create mode 100644 tools/Migrate_01/SquidexMigrations.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs index 885a8936f..6f4f99495 100644 --- a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs +++ b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs @@ -5,9 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Reflection; + namespace Squidex.Domain.Apps.Core { public static class SquidexCoreModel { + public static readonly Assembly Assembly = typeof(SquidexCoreModel).Assembly; } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 0319305bc..eba2e0b88 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.OData.UriParser; -using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs index 906a579bf..d49cb8921 100644 --- a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs +++ b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs @@ -5,9 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Reflection; + namespace Squidex.Domain.Apps.Events { public static class SquidexEvents { + public static readonly Assembly Assembly = typeof(SquidexEvents).Assembly; } } diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs index 6bf4f004d..203dd0050 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Concurrent; -using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; @@ -34,73 +33,65 @@ namespace Squidex.Infrastructure.EventSourcing this.projectionHost = projectionHost; } - private string CreateFilterStreamName(string filter) + private string CreateFilterProjectionName(string filter) { - return $"by-{StreamByFilter}-{prefix.Simplify()}-{filter.Simplify()}"; + return $"by-{prefix.Simplify()}-{filter.Simplify()}"; } - private string CreatePropertyStreamName(string property) + private string CreatePropertyProjectionName(string property) { - return $"by-{StreamByFilter}-{prefix.Simplify()}-{property.Simplify()}-property"; + return $"by-{prefix.Simplify()}-{property.Simplify()}-property"; } public async Task CreateProjectionAsync(string property, object value) { - var streamName = CreatePropertyStreamName(property); - - if (projections.TryAdd(streamName, true)) - { - var projectionConfig = - $@"fromAll() - .when({{ - $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && e.data.{property}) {{ - linkTo('{streamName}-' + e.data.{property}, e); - }} + var name = CreatePropertyProjectionName(property); + + var query = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && e.data.{property}) {{ + linkTo('{name}-' + e.data.{property}, e); }} - }});"; - - try - { - var credentials = connection.Settings.DefaultUserCredentials; + }} + }});"; - await projectionsManager.CreateContinuousAsync($"{streamName}", projectionConfig, credentials); - } - catch (Exception ex) - { - if (!ex.Is()) - { - throw; - } - } - } + await CreateProjectionAsync(name, query); - return $"{streamName}-{value}"; + return $"{name}-{value}"; } public async Task CreateProjectionAsync(string streamFilter = null) { streamFilter = streamFilter ?? ".*"; - var streamName = CreateFilterStreamName(streamFilter); + var name = CreateFilterProjectionName(streamFilter); - if (projections.TryAdd(streamName, true)) - { - var projectionConfig = - $@"fromAll() - .when({{ - $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ - linkTo('{streamName}', e); - }} + var query = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ + linkTo('{name}', e); }} - }});"; + }} + }});"; + + await CreateProjectionAsync(name, query); + return name; + } + + private async Task CreateProjectionAsync(string name, string query) + { + if (projections.TryAdd(name, true)) + { try { var credentials = connection.Settings.DefaultUserCredentials; - await projectionsManager.CreateContinuousAsync($"{streamName}", projectionConfig, credentials); + await projectionsManager.CreateContinuousAsync(name, query, credentials); } catch (Exception ex) { @@ -110,8 +101,6 @@ namespace Squidex.Infrastructure.EventSourcing } } } - - return streamName; } public async Task ConnectAsync() diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs index 81649a439..dfff1ef94 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json.Linq; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index 9c0e3f877..016d3d837 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -21,18 +21,12 @@ namespace Squidex.Infrastructure.EventSourcing public Task AppendAsync(Guid commitId, string streamName, ICollection events) { - return AppendEventsInternalAsync(commitId, streamName, EtagVersion.Any, events); + return AppendAsync(commitId, streamName, EtagVersion.Any, events); } - public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) { Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); - - return AppendEventsInternalAsync(commitId, streamName, expectedVersion, events); - } - - private async Task AppendEventsInternalAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { Guard.NotNullOrEmpty(streamName, nameof(streamName)); Guard.NotNull(events, nameof(events)); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index b05f80515..27cfdca13 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -8,7 +8,6 @@ using System; using System.Globalization; using System.Threading.Tasks; -using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Tasks; diff --git a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs index 83ce9ed5a..72f01b63a 100644 --- a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs +++ b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.EventSourcing var eventType = typeNameRegistry.GetType(eventData.Type); var headers = eventData.Metadata.ToObject(); - var content = eventData.Metadata.ToObject(eventType, serializer) as IEvent; + var content = eventData.Payload.ToObject(eventType, serializer) as IEvent; if (migrate && content is IMigratedEvent migratedEvent) { diff --git a/src/Squidex.Infrastructure/Migrations/IMigration.cs b/src/Squidex.Infrastructure/Migrations/IMigration.cs index 89d698688..3a837e088 100644 --- a/src/Squidex.Infrastructure/Migrations/IMigration.cs +++ b/src/Squidex.Infrastructure/Migrations/IMigration.cs @@ -5,17 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Threading.Tasks; namespace Squidex.Infrastructure.Migrations { public interface IMigration { - int FromVersion { get; } - - int ToVersion { get; } - - Task UpdateAsync(IEnumerable previousMigrations); + Task UpdateAsync(); } } diff --git a/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs b/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs new file mode 100644 index 000000000..3992f953e --- /dev/null +++ b/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Migrations +{ + public interface IMigrationPath + { + (int Version, IEnumerable Migrations) GetNext(int version); + } +} diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs index 4ad2f4065..e0f8feffe 100644 --- a/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure.Log; @@ -15,20 +13,20 @@ namespace Squidex.Infrastructure.Migrations { public sealed class Migrator { - private readonly IMigrationStatus migrationStatus; - private readonly IEnumerable migrations; private readonly ISemanticLog log; + private readonly IMigrationStatus migrationStatus; + private readonly IMigrationPath migrationPath; public int LockWaitMs { get; set; } = 5000; - public Migrator(IMigrationStatus migrationStatus, IEnumerable migrations, ISemanticLog log) + public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, ISemanticLog log) { Guard.NotNull(migrationStatus, nameof(migrationStatus)); - Guard.NotNull(migrations, nameof(migrations)); + Guard.NotNull(migrationPath, nameof(migrationPath)); Guard.NotNull(log, nameof(log)); this.migrationStatus = migrationStatus; - this.migrations = migrations.OrderByDescending(x => x.ToVersion).ToList(); + this.migrationPath = migrationPath; this.log = log; } @@ -39,8 +37,6 @@ namespace Squidex.Infrastructure.Migrations try { - var lastMigrator = migrations.FirstOrDefault(); - while (!await migrationStatus.TryLockAsync()) { log.LogInformation(w => w @@ -52,13 +48,16 @@ namespace Squidex.Infrastructure.Migrations version = await migrationStatus.GetVersionAsync(); - if (lastMigrator != null && lastMigrator.ToVersion != version) + while (true) { - var migrationPath = FindMigratorPath(version, lastMigrator.ToVersion).ToList(); + var migrationStep = migrationPath.GetNext(version); - var previousMigrations = new List(); + if (migrationStep.Migrations == null || !migrationStep.Migrations.Any()) + { + break; + } - foreach (var migration in migrationPath) + foreach (var migration in migrationStep.Migrations) { var name = migration.GetType().ToString(); @@ -72,13 +71,11 @@ namespace Squidex.Infrastructure.Migrations .WriteProperty("status", "Completed") .WriteProperty("migrator", name))) { - await migration.UpdateAsync(previousMigrations.ToList()); - - version = migration.ToVersion; + await migration.UpdateAsync(); } - - previousMigrations.Add(migration); } + + version = migrationStep.Version; } } finally @@ -86,30 +83,5 @@ namespace Squidex.Infrastructure.Migrations await migrationStatus.UnlockAsync(version); } } - - private IEnumerable FindMigratorPath(int fromVersion, int toVersion) - { - var addedMigrators = new HashSet(); - - while (true) - { - var bestMigrator = migrations.Where(x => x.FromVersion < x.ToVersion).FirstOrDefault(x => x.FromVersion == fromVersion); - - if (bestMigrator != null && addedMigrators.Add(bestMigrator)) - { - fromVersion = bestMigrator.ToVersion; - - yield return bestMigrator; - } - else if (fromVersion != toVersion) - { - throw new InvalidOperationException($"There is no migration path from {fromVersion} to {toVersion}."); - } - else - { - break; - } - } - } } } diff --git a/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/src/Squidex.Infrastructure/SquidexInfrastructure.cs index 11cfb6764..8dac58f91 100644 --- a/src/Squidex.Infrastructure/SquidexInfrastructure.cs +++ b/src/Squidex.Infrastructure/SquidexInfrastructure.cs @@ -5,9 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Reflection; + namespace Squidex.Infrastructure { public static class SquidexInfrastructure { + public static readonly Assembly Assembly = typeof(SquidexInfrastructure).Assembly; } } diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index c8f3cbb12..f783f2601 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -28,10 +28,10 @@ namespace Squidex.Config.Domain { private static readonly TypeNameRegistry TypeNameRegistry = new TypeNameRegistry() - .MapUnmapped(typeof(Migration01_FromCqrs).Assembly) - .MapUnmapped(typeof(SquidexCoreModel).Assembly) - .MapUnmapped(typeof(SquidexEvents).Assembly) - .MapUnmapped(typeof(SquidexInfrastructure).Assembly); + .MapUnmapped(SquidexCoreModel.Assembly) + .MapUnmapped(SquidexEvents.Assembly) + .MapUnmapped(SquidexInfrastructure.Assembly) + .MapUnmapped(SquidexMigrations.Assembly); private static readonly FieldRegistry FieldRegistry = new FieldRegistry(TypeNameRegistry); diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index 263fdf204..fd498d374 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -9,6 +9,7 @@ using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Migrate_01; +using Migrate_01.Migrations; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Apps; @@ -63,13 +64,19 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddTransientAs() + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() .As(); - services.AddTransientAs() + services.AddTransientAs() .As(); - services.AddTransientAs() + services.AddTransientAs() .As(); services.AddTransientAs() diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs similarity index 97% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs rename to tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs index 6d9406d95..aef438ac2 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs @@ -15,7 +15,7 @@ using Xunit; namespace Squidex.Infrastructure.EventSourcing { - public class EventDataFormatterTests + public class DefaultEventDataFormatterTests { public sealed class MyOldEvent : IEvent, IMigratedEvent { @@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.EventSourcing private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); private readonly DefaultEventDataFormatter sut; - public EventDataFormatterTests() + public DefaultEventDataFormatterTests() { serializerSettings.Converters.Add(new PropertiesBagConverter()); diff --git a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs index 930298678..5cf285060 100644 --- a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -19,7 +19,9 @@ namespace Squidex.Infrastructure.Migrations public sealed class MigratorTests { private readonly IMigrationStatus status = A.Fake(); + private readonly IMigrationPath path = A.Fake(); private readonly ISemanticLog log = A.Fake(); + private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); public sealed class InMemoryStatus : IMigrationStatus { @@ -64,6 +66,14 @@ namespace Squidex.Infrastructure.Migrations public MigratorTests() { + A.CallTo(() => path.GetNext(A.Ignored)) + .ReturnsLazily((int v) => + { + var m = migrations.Where(x => x.From == v).ToList(); + + return m.Count == 0 ? (0, null) : (migrations.Max(x => x.To), migrations.Select(x => x.Migration)); + }); + A.CallTo(() => status.GetVersionAsync()).Returns(0); A.CallTo(() => status.TryLockAsync()).Returns(true); } @@ -75,13 +85,13 @@ namespace Squidex.Infrastructure.Migrations var migrator_1_2 = BuildMigration(1, 2); var migrator_2_3 = BuildMigration(2, 3); - var migrator = new Migrator(status, new[] { migrator_0_1, migrator_1_2, migrator_2_3 }, log); + var sut = new Migrator(status, path, log); - await migrator.MigrateAsync(); + await sut.MigrateAsync(); - A.CallTo(() => migrator_0_1.UpdateAsync(A>.That.IsEmpty())).MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A>.That.IsSameSequenceAs(migrator_0_1))).MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(A>.That.IsSameSequenceAs(migrator_0_1, migrator_1_2))).MustHaveHappened(); + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); A.CallTo(() => status.UnlockAsync(3)).MustHaveHappened(); } @@ -93,51 +103,15 @@ namespace Squidex.Infrastructure.Migrations var migrator_1_2 = BuildMigration(1, 2); var migrator_2_3 = BuildMigration(2, 3); - var migrator = new Migrator(status, new[] { migrator_0_1, migrator_1_2, migrator_2_3 }, log); - - A.CallTo(() => migrator_1_2.UpdateAsync(A>.Ignored)).Throws(new ArgumentException()); - - await Assert.ThrowsAsync(migrator.MigrateAsync); - - A.CallTo(() => migrator_0_1.UpdateAsync(A>.That.IsEmpty())).MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A>.That.IsSameSequenceAs(migrator_0_1))).MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(A>.Ignored)).MustNotHaveHappened(); - - A.CallTo(() => status.UnlockAsync(1)).MustHaveHappened(); - } - - [Fact] - public async Task Should_migrate_with_fastest_path() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_0_2 = BuildMigration(0, 2); - var migrator_1_2 = BuildMigration(1, 2); - var migrator_2_3 = BuildMigration(2, 3); - - var migrator = new Migrator(status, new[] { migrator_0_1, migrator_0_2, migrator_1_2, migrator_2_3 }, log); - - await migrator.MigrateAsync(); - - A.CallTo(() => migrator_0_2.UpdateAsync(A>.That.IsEmpty())).MustHaveHappened(); - A.CallTo(() => migrator_0_1.UpdateAsync(A>.Ignored)).MustNotHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A>.Ignored)).MustNotHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(A>.That.IsSameSequenceAs(migrator_0_2))).MustHaveHappened(); - - A.CallTo(() => status.UnlockAsync(3)).MustHaveHappened(); - } - - [Fact] - public async Task Should_throw_if_no_path_found() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_2_3 = BuildMigration(2, 3); + var sut = new Migrator(status, path, log); - var migrator = new Migrator(status, new[] { migrator_0_1, migrator_2_3 }, log); + A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); - await Assert.ThrowsAsync(migrator.MigrateAsync); + await Assert.ThrowsAsync(sut.MigrateAsync); - A.CallTo(() => migrator_0_1.UpdateAsync(A>.Ignored)).MustNotHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(A>.Ignored)).MustNotHaveHappened(); + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); } @@ -148,20 +122,19 @@ namespace Squidex.Infrastructure.Migrations var migrator_0_1 = BuildMigration(0, 1); var migrator_1_2 = BuildMigration(1, 2); - var migrator = new Migrator(new InMemoryStatus(), new[] { migrator_0_1, migrator_1_2 }, log) { LockWaitMs = 2 }; + var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; - await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(migrator.MigrateAsync))); + await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(sut.MigrateAsync))); - A.CallTo(() => migrator_0_1.UpdateAsync(A>.Ignored)).MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => migrator_1_2.UpdateAsync(A>.Ignored)).MustHaveHappened(Repeated.Exactly.Once); + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(Repeated.Exactly.Once); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(Repeated.Exactly.Once); } private IMigration BuildMigration(int fromVersion, int toVersion) { var migration = A.Fake(); - A.CallTo(() => migration.FromVersion).Returns(fromVersion); - A.CallTo(() => migration.ToVersion).Returns(toVersion); + migrations.Add((fromVersion, toVersion, migration)); return migration; } diff --git a/tools/Migrate_01/Migrate_01.csproj b/tools/Migrate_01/Migrate_01.csproj index c5b80e7f5..9db8eb6c0 100644 --- a/tools/Migrate_01/Migrate_01.csproj +++ b/tools/Migrate_01/Migrate_01.csproj @@ -6,6 +6,7 @@ + diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs new file mode 100644 index 000000000..682c3c704 --- /dev/null +++ b/tools/Migrate_01/MigrationPath.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Migrate_01.Migrations; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01 +{ + public class MigrationMatrix + { + private readonly IServiceProvider serviceProvider; + + public MigrationMatrix(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public (int Version, IEnumerable Migrations) MigrationPath(int version) + { + switch (version) + { + case 0: + return (4, + new IMigration[] + { + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() + }); + case 1: + return (4, + new IMigration[] + { + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() + }); + case 2: + return (4, + new IMigration[] + { + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() + }); + case 3: + return (4, + new IMigration[] + { + serviceProvider.GetRequiredService() + }); + } + + return (0, null); + } + } +} diff --git a/tools/Migrate_01/Migration02_AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs similarity index 83% rename from tools/Migrate_01/Migration02_AddPatterns.cs rename to tools/Migrate_01/Migrations/AddPatterns.cs index 279ce58cd..ebafe2232 100644 --- a/tools/Migrate_01/Migration02_AddPatterns.cs +++ b/tools/Migrate_01/Migrations/AddPatterns.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; @@ -15,26 +14,22 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.States; -namespace Migrate_01 +namespace Migrate_01.Migrations { - public sealed class Migration02_AddPatterns : IMigration + public sealed class 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, IAppRepository appRepository, IStateFactory stateFactory) + public AddPatterns(InitialPatterns initialPatterns, IAppRepository appRepository, IStateFactory stateFactory) { this.initialPatterns = initialPatterns; this.appRepository = appRepository; this.stateFactory = stateFactory; } - public async Task UpdateAsync(IEnumerable previousMigrations) + public async Task UpdateAsync() { var ids = await appRepository.QueryAppIdsAsync(); diff --git a/tools/Migrate_01/Migration00_ConvertEventStore.cs b/tools/Migrate_01/Migrations/ConvertEventStore.cs similarity index 80% rename from tools/Migrate_01/Migration00_ConvertEventStore.cs rename to tools/Migrate_01/Migrations/ConvertEventStore.cs index 4cac5ed83..83715a400 100644 --- a/tools/Migrate_01/Migration00_ConvertEventStore.cs +++ b/tools/Migrate_01/Migrations/ConvertEventStore.cs @@ -5,29 +5,24 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; -namespace Migrate_01 +namespace Migrate_01.Migrations { - public sealed class Migration00_ConvertEventStore : IMigration + public sealed class ConvertEventStore : IMigration { private readonly IEventStore eventStore; - public int FromVersion { get; } = 0; - - public int ToVersion { get; } = 1; - - public Migration00_ConvertEventStore(IEventStore eventStore) + public ConvertEventStore(IEventStore eventStore) { this.eventStore = eventStore; } - public async Task UpdateAsync(IEnumerable previousMigrations) + public async Task UpdateAsync() { if (eventStore is MongoEventStore mongoEventStore) { diff --git a/tools/Migrate_01/Migration03_SplitContentCollections.cs b/tools/Migrate_01/Migrations/RebuildContentCollections.cs similarity index 52% rename from tools/Migrate_01/Migration03_SplitContentCollections.cs rename to tools/Migrate_01/Migrations/RebuildContentCollections.cs index 98ba42784..a633f79c9 100644 --- a/tools/Migrate_01/Migration03_SplitContentCollections.cs +++ b/tools/Migrate_01/Migrations/RebuildContentCollections.cs @@ -5,32 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure.Migrations; -namespace Migrate_01 +namespace Migrate_01.Migrations { - public class Migration03_SplitContentCollections : IMigration + public class RebuildContentCollections : IMigration { private readonly Rebuilder rebuilder; - public int FromVersion { get; } = 2; - - public int ToVersion { get; } = 3; - - public Migration03_SplitContentCollections(Rebuilder rebuilder) + public RebuildContentCollections(Rebuilder rebuilder) { this.rebuilder = rebuilder; } - public async Task UpdateAsync(IEnumerable previousMigrations) + public Task UpdateAsync() { - if (!previousMigrations.Any(x => x is Migration01_FromCqrs)) - { - await rebuilder.RebuildContentAsync(); - } + return rebuilder.RebuildContentAsync(); } } } diff --git a/tools/Migrate_01/Migration01_FromCqrs.cs b/tools/Migrate_01/Migrations/RebuildSnapshots.cs similarity index 68% rename from tools/Migrate_01/Migration01_FromCqrs.cs rename to tools/Migrate_01/Migrations/RebuildSnapshots.cs index f54f4d188..b5a03a4e4 100644 --- a/tools/Migrate_01/Migration01_FromCqrs.cs +++ b/tools/Migrate_01/Migrations/RebuildSnapshots.cs @@ -5,26 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.Migrations; -namespace Migrate_01 +namespace Migrate_01.Migrations { - public sealed class Migration01_FromCqrs : IMigration + public sealed class RebuildSnapshots : IMigration { private readonly Rebuilder rebuilder; - public int FromVersion { get; } = 0; - - public int ToVersion { get; } = 1; - - public Migration01_FromCqrs(Rebuilder rebuilder) + public RebuildSnapshots(Rebuilder rebuilder) { this.rebuilder = rebuilder; } - public async Task UpdateAsync(IEnumerable previousMigrations) + public async Task UpdateAsync() { await rebuilder.RebuildConfigAsync(); await rebuilder.RebuildContentAsync(); diff --git a/tools/Migrate_01/SquidexMigrations.cs b/tools/Migrate_01/SquidexMigrations.cs new file mode 100644 index 000000000..085bcf9dd --- /dev/null +++ b/tools/Migrate_01/SquidexMigrations.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Reflection; + +namespace Migrate_01 +{ + public static class SquidexMigrations + { + public static readonly Assembly Assembly = typeof(SquidexMigrations).Assembly; + } +} From c095b3636ecda70d72fb957be76a1f7cc44c8ce9 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 15 Jan 2018 12:57:40 +0100 Subject: [PATCH 04/26] Migration completed --- .../Apps/AppDomainObject.cs | 3 +- .../Assets/AssetDomainObject.cs | 3 +- .../Contents/ContentDomainObject.cs | 3 +- .../Rules/RuleDomainObject.cs | 3 +- .../Schemas/SchemaDomainObject.cs | 3 +- .../SquidexDomainObjectBase.cs | 26 +++++++++++++++++ .../SquidexHeaderExtensions.cs | 28 +++++++++++++++++++ .../SquidexHeaders.cs | 14 ++++++++++ .../EventSourcing/ProjectionClient.cs | 4 +-- .../EventSourcing/MongoEvent.cs | 13 +++------ .../EventSourcing/MongoEventStore_Reader.cs | 2 +- .../EventSourcing/MongoEventStore_Writer.cs | 2 +- .../MongoDb/BsonJsonConvention.cs | 1 + .../Commands/DomainObjectBase.cs | 4 +-- .../Json/NamedGuidIdConverter.cs | 13 +++------ .../Json/NamedLongIdConverter.cs | 13 +++------ .../Migrations/Migrator.cs | 2 +- src/Squidex.Infrastructure/NamedId.cs | 22 +++++++++++++++ tools/Migrate_01/MigrationPath.cs | 6 ++-- .../Migrations/ConvertEventStore.cs | 17 +++++++++-- 20 files changed, 132 insertions(+), 50 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs create mode 100644 src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Events/SquidexHeaders.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index 411f4a8e8..2dedb3a7a 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -13,13 +13,12 @@ using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Apps { - public sealed class AppDomainObject : DomainObjectBase + public sealed class AppDomainObject : SquidexDomainObjectBase { private readonly InitialPatterns initialPatterns; diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index 103cd9981..360f9134c 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -9,13 +9,12 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetDomainObject : DomainObjectBase + public sealed class AssetDomainObject : SquidexDomainObjectBase { public AssetDomainObject Create(CreateAsset command) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index 0b46b12d7..d9eb1fa1d 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -10,13 +10,12 @@ using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentDomainObject : DomainObjectBase + public sealed class ContentDomainObject : SquidexDomainObjectBase { public ContentDomainObject Create(CreateContent command) { diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index bc05ac8bd..706b3e352 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -9,13 +9,12 @@ using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.State; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Rules { - public sealed class RuleDomainObject : DomainObjectBase + public sealed class RuleDomainObject : SquidexDomainObjectBase { public void Create(CreateRule command) { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index cc081ee28..d4c862953 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -12,13 +12,12 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.State; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Schemas { - public sealed class SchemaDomainObject : DomainObjectBase + public sealed class SchemaDomainObject : SquidexDomainObjectBase { private readonly FieldRegistry registry; diff --git a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs new file mode 100644 index 000000000..63f0f94ed --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities +{ + public abstract class SquidexDomainObjectBase : DomainObjectBase where T : IDomainState, new() + { + public override void RaiseEvent(Envelope @event) + { + if (@event.Payload is AppEvent appEvent) + { + @event.SetAppId(appEvent.AppId.Id); + } + + base.RaiseEvent(@event); + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs b/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs new file mode 100644 index 000000000..0673d3b5b --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Globalization; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events +{ + public static class SquidexHeaderExtensions + { + public static Guid AppId(this EnvelopeHeaders headers) + { + return headers[SquidexHeaders.AppId].ToGuid(CultureInfo.InvariantCulture); + } + + public static Envelope SetAppId(this Envelope envelope, Guid value) where T : class + { + envelope.Headers.Set(SquidexHeaders.AppId, value); + + return envelope; + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs b/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs new file mode 100644 index 000000000..e2f610a10 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Events +{ + public static class SquidexHeaders + { + public static readonly string AppId = "AppId"; + } +} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs index 203dd0050..c4c41ef6d 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs @@ -51,8 +51,8 @@ namespace Squidex.Infrastructure.EventSourcing $@"fromAll() .when({{ $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && e.data.{property}) {{ - linkTo('{name}-' + e.data.{property}, e); + if (e.streamId.indexOf('{prefix}') === 0 && e.metadata.{property}) {{ + linkTo('{name}-' + e.metadata.{property}, e); }} }} }});"; diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs index dfff1ef94..62d15ca20 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs @@ -7,7 +7,6 @@ using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json.Linq; -using Squidex.Infrastructure.Reflection; namespace Squidex.Infrastructure.EventSourcing { @@ -15,7 +14,7 @@ namespace Squidex.Infrastructure.EventSourcing { [BsonElement] [BsonRequired] - public JToken Payload { get; set; } + public string Payload { get; set; } [BsonElement] [BsonRequired] @@ -25,18 +24,14 @@ namespace Squidex.Infrastructure.EventSourcing [BsonRequired] public string Type { get; set; } - public MongoEvent() + public static MongoEvent FromEventData(EventData data) { - } - - public MongoEvent(EventData data) - { - SimpleMapper.Map(data, this); + return new MongoEvent { Type = data.Type, Metadata = data.Metadata, Payload = data.ToString() }; } public EventData ToEventData() { - return SimpleMapper.Map(this, new EventData()); + return new EventData { Type = Type, Metadata = Metadata, Payload = JObject.Parse(Payload) }; } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index ccc7737e9..eed2d0bce 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -167,7 +167,7 @@ namespace Squidex.Infrastructure.EventSourcing private static string CreateIndexPath(string property) { - return $"Events.Payload.{property}"; + return $"Events.Metadata.{property}"; } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index 016d3d837..937b66050 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -108,7 +108,7 @@ namespace Squidex.Infrastructure.EventSourcing foreach (var e in events) { - var mongoEvent = new MongoEvent(e); + var mongoEvent = MongoEvent.FromEventData(e); commitEvents[i++] = mongoEvent; } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index 2a8d6e572..2d2dc8939 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -12,6 +12,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.MongoDb { diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 7327a058d..b054d8729 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -45,13 +45,13 @@ namespace Squidex.Infrastructure.Commands RaiseEvent(Envelope.Create(@event)); } - public void RaiseEvent(Envelope @event) where TEvent : class, IEvent + public virtual void RaiseEvent(Envelope @event) { Guard.NotNull(@event, nameof(@event)); @event.SetAggregateId(id); - ApplyEvent(@event.To()); + ApplyEvent(@event); snapshot.Version++; diff --git a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs index 4499419d3..c4c83790f 100644 --- a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs @@ -25,19 +25,14 @@ namespace Squidex.Infrastructure.Json throw new JsonException($"Expected String, but got {reader.TokenType}."); } - var parts = reader.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length < 2) + try { - throw new JsonException("Named id must have more than 2 parts divided by commata."); + return NamedId.Parse(reader.Value.ToString(), Guid.TryParse); } - - if (!Guid.TryParse(parts[0], out var id)) + catch (ArgumentException ex) { - throw new JsonException("Named id must be a valid guid."); + throw new JsonException(ex.Message); } - - return new NamedId(id, string.Join(",", parts.Skip(1))); } } } diff --git a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs index 4efb97dc0..5054b4dc7 100644 --- a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs @@ -25,19 +25,14 @@ namespace Squidex.Infrastructure.Json throw new JsonException($"Expected String, but got {reader.TokenType}."); } - var parts = reader.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length < 2) + try { - throw new JsonException("Named id must have more than 2 parts divided by commata."); + return NamedId.Parse(reader.Value.ToString(), long.TryParse); } - - if (!long.TryParse(parts[0], out var id)) + catch (ArgumentException ex) { - throw new JsonException("Named id must be a valid long."); + throw new JsonException(ex.Message); } - - return new NamedId(id, string.Join(",", parts.Skip(1))); } } } diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs index e0f8feffe..0ba832d9e 100644 --- a/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.Migrations private readonly IMigrationStatus migrationStatus; private readonly IMigrationPath migrationPath; - public int LockWaitMs { get; set; } = 5000; + public int LockWaitMs { get; set; } = 500; public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, ISemanticLog log) { diff --git a/src/Squidex.Infrastructure/NamedId.cs b/src/Squidex.Infrastructure/NamedId.cs index 0b16475fe..e8f99f6d4 100644 --- a/src/Squidex.Infrastructure/NamedId.cs +++ b/src/Squidex.Infrastructure/NamedId.cs @@ -6,9 +6,12 @@ // ========================================================================== using System; +using System.Linq; namespace Squidex.Infrastructure { + public delegate bool Parser(string input, out T result); + public sealed class NamedId : IEquatable> { public T Id { get; } @@ -44,5 +47,24 @@ namespace Squidex.Infrastructure { return (Id.GetHashCode() * 397) ^ Name.GetHashCode(); } + + public static NamedId Parse(string value, Parser parser) + { + Guard.NotNull(value, nameof(value)); + + var parts = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length < 2) + { + throw new ArgumentException("Named id must have more than 2 parts divided by commata."); + } + + if (!parser(parts[0], out var id)) + { + throw new ArgumentException("Named id must be a valid guid."); + } + + return new NamedId(id, string.Join(",", parts.Skip(1))); + } } } diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs index 682c3c704..365bdf896 100644 --- a/tools/Migrate_01/MigrationPath.cs +++ b/tools/Migrate_01/MigrationPath.cs @@ -13,16 +13,16 @@ using Squidex.Infrastructure.Migrations; namespace Migrate_01 { - public class MigrationMatrix + public sealed class MigrationPath : IMigrationPath { private readonly IServiceProvider serviceProvider; - public MigrationMatrix(IServiceProvider serviceProvider) + public MigrationPath(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; } - public (int Version, IEnumerable Migrations) MigrationPath(int version) + public (int Version, IEnumerable Migrations) GetNext(int version) { switch (version) { diff --git a/tools/Migrate_01/Migrations/ConvertEventStore.cs b/tools/Migrate_01/Migrations/ConvertEventStore.cs index 83715a400..20c74e8ab 100644 --- a/tools/Migrate_01/Migrations/ConvertEventStore.cs +++ b/tools/Migrate_01/Migrations/ConvertEventStore.cs @@ -5,11 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; namespace Migrate_01.Migrations { @@ -34,10 +39,16 @@ namespace Migrate_01.Migrations { foreach (BsonDocument @event in commit["Events"].AsBsonArray) { - @event.Remove("EventId"); + var meta = JObject.Parse(@event["Metadata"].AsString); + var data = JObject.Parse(@event["Payload"].AsString); + + if (data.TryGetValue("appId", out var appId)) + { + meta[SquidexHeaders.AppId] = NamedId.Parse(appId.ToString(), Guid.TryParse).Id; + } - @event["Payload"] = BsonDocument.Parse(@event["Payload"].AsString); - @event["Metadata"] = BsonDocument.Parse(@event["Metadata"].AsString); + @event.Remove("EventId"); + @event["Metadata"] = meta.ToBson(); } await collection.ReplaceOneAsync(filter.Eq("_id", commit["_id"].AsString), commit); From c6c0250a5b625ce1fc8a01ba2a1302646d86baf2 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 15 Jan 2018 17:06:35 +0100 Subject: [PATCH 05/26] Cleanup --- .../MongoDb/BsonJsonConvention.cs | 1 - .../Commands/DomainObjectBase.cs | 10 +--------- .../Json/NamedGuidIdConverter.cs | 1 - .../Json/NamedLongIdConverter.cs | 1 - .../Commands/DomainObjectBaseTests.cs | 17 ----------------- 5 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index 2d2dc8939..2a8d6e572 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -12,7 +12,6 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.MongoDb { diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index b054d8729..c4d0e0ef4 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; @@ -53,9 +52,7 @@ namespace Squidex.Infrastructure.Commands ApplyEvent(@event); - snapshot.Version++; - - uncomittedEvents.Add(@event.To()); + uncomittedEvents.Add(@event); } public IReadOnlyList> GetUncomittedEvents() @@ -79,11 +76,6 @@ namespace Squidex.Infrastructure.Commands public Task WriteSnapshotAsync() { - if (persistence.Version == EtagVersion.NotFound) - { - Debugger.Break(); - } - snapshot.Version = persistence.Version; return persistence.WriteSnapshotAsync(snapshot); diff --git a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs index c4c83790f..1695a96f9 100644 --- a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Linq; using Newtonsoft.Json; namespace Squidex.Infrastructure.Json diff --git a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs index 5054b4dc7..cc85acd9b 100644 --- a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Linq; using Newtonsoft.Json; namespace Squidex.Infrastructure.Json diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 678ec9d4a..70e23f0b8 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -36,23 +36,6 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(EtagVersion.Empty, sut.Version); } - [Fact] - public void Should_add_event_to_uncommitted_events_and_increase_version_when_raised() - { - var event1 = new MyEvent(); - var event2 = new MyEvent(); - - sut.RaiseEvent(event1); - sut.RaiseEvent(event2); - - Assert.Equal(1, sut.Version); - Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); - - sut.ClearUncommittedEvents(); - - Assert.Equal(0, sut.GetUncomittedEvents().Count); - } - [Fact] public async Task Should_write_state_and_events_when_saved() { From a92d4a2fd0825373c6ce77f72505fb5c425faa13 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 8 Feb 2018 11:04:18 +0100 Subject: [PATCH 06/26] Bring back the schema selection. --- .../schema/types/references-validation.component.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html index b8c1705a4..45de2781a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html @@ -1,4 +1,15 @@
+
+ + +
+ +
+
+ +
From 7d2cd0897de6674e44cb8edbb3b120e7889925d8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 8 Feb 2018 11:22:37 +0100 Subject: [PATCH 07/26] Do not show schemas from other apps. --- src/Squidex.Domain.Apps.Entities/AppProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 38f0b2742..ace83d66c 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities { var schema = await stateFactory.GetSingleAsync(id); - if (!IsFound(schema)) + if (!IsFound(schema) || schema.Snapshot.IsDeleted || schema.Snapshot.AppId != appId) { return null; } From 24499dd8352be8af9ef1b76cfabe87f88e10a34c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 8 Feb 2018 11:36:32 +0100 Subject: [PATCH 08/26] Minor UI improvement. --- .../content/pages/contents/contents-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index fa9efefed..9589b5b6b 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -56,7 +56,7 @@

- Refs + References

From c22f7126688c70a438ab3ea5e70784d639e7ffc1 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 11:18:40 +0100 Subject: [PATCH 09/26] Minor script improvements. --- .../ContentWrapper/ContentDataObject.cs | 2 +- .../ContentWrapper/ContentDataProperty.cs | 2 +- .../Scripting/ContentDataObjectTests.cs | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs index 934ee5387..79b1e14c4 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs @@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { EnsurePropertiesInitialized(); - return fieldProperties.GetOrDefault(propertyName) ?? new PropertyDescriptor(new ObjectInstance(Engine) { Extensible = true }, true, false, true); + return fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this, new ContentFieldObject(this, new ContentFieldData(), false))); } public override IEnumerable> GetOwnProperties() diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs index 943d41240..4ea31b9ea 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { if (value == null || !value.IsObject()) { - throw new JavaScriptException("Can only assign object to content data."); + throw new JavaScriptException("You can only assign objects to content data."); } var obj = value.AsObject(); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs index 9c5f3c7ee..592e0765e 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs @@ -32,6 +32,22 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting Assert.Equal(expected, result); } + [Fact] + public void Should_update_data_when_setting_lazy_field() + { + var original = new NamedContentData(); + + var expected = + new NamedContentData() + .AddField("number", + new ContentFieldData() + .AddValue("iv", 1.0)); + + var result = ExecuteScript(original, @"data.number.iv = 1"); + + Assert.Equal(expected, result); + } + [Fact] public void Should_update_data_defining_property_for_content() { From 2f25c6b27a9f9eac1c739ce7f48708394297dc23 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 11:33:42 +0100 Subject: [PATCH 10/26] 1) Remove model validation because we validate at the command level. 2) Option to publish schema in one api call. --- .../Templates/CreateBlogCommandMiddleware.cs | 13 ++++++----- .../Schemas/Commands/CreateSchema.cs | 2 ++ .../Schemas/State/SchemaState.cs | 5 ++++ .../Schemas/SchemaCreated.cs | 2 ++ .../Schemas/Models/CreateSchemaDto.cs | 5 ++++ .../Pipeline/ApiExceptionFilterAttribute.cs | 23 +------------------ 6 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs index 93e9747b9..6eeac4e55 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs @@ -105,6 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates var command = new CreateSchema { Name = "posts", + Publish = true, Properties = new SchemaProperties { Label = "Posts" @@ -136,8 +137,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates IsListField = true, MaxLength = 100, MinLength = 0, - Label = "Slug" - } + Label = "Slug (Autogenerated)" + }, + IsDisabled = true }, new CreateSchemaField { @@ -158,7 +160,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates var schemaId = new NamedId(command.SchemaId, command.Name); - await publishAsync(new PublishSchema { SchemaId = schemaId }); await publishAsync(new ConfigureScripts { SchemaId = schemaId, @@ -205,8 +206,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates IsListField = true, MaxLength = 100, MinLength = 0, - Label = "Slug" - } + Label = "Slug (Autogenerated)" + }, + IsDisabled = true }, new CreateSchemaField { @@ -227,7 +229,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates var schemaId = new NamedId(command.SchemaId, command.Name); - await publishAsync(new PublishSchema { SchemaId = schemaId }); await publishAsync(new ConfigureScripts { SchemaId = schemaId, diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs index f303e5c95..afbd735f5 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -20,6 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public SchemaProperties Properties { get; set; } + public bool Publish { get; set; } + public string Name { get; set; } Guid IAggregateCommand.AggregateId diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 5a8755b82..5d06deb3b 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -70,6 +70,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State schema = schema.Update(@event.Properties); } + if (@event.Publish) + { + schema = schema.Publish(); + } + if (@event.Fields != null) { foreach (var eventField in @event.Fields) diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs b/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs index c44595bbc..2e7c6ec2b 100644 --- a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs @@ -19,5 +19,7 @@ namespace Squidex.Domain.Apps.Events.Schemas public SchemaFields Fields { get; set; } public SchemaProperties Properties { get; set; } + + public bool Publish { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs index 80b0d9a3b..cb6a04ae7 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs @@ -28,5 +28,10 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// Optional fields. /// public List Fields { get; set; } + + /// + /// Set it to true to autopublish the schema. + /// + public bool Publish { get; set; } } } diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index ae8ec9899..c0248029e 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -62,28 +62,7 @@ namespace Squidex.Pipeline error.StatusCode = statusCode; return new ObjectResult(error) { StatusCode = statusCode }; - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - if (!context.ModelState.IsValid) - { - var errors = new List(); - - foreach (var m in context.ModelState) - { - foreach (var e in m.Value.Errors) - { - if (!string.IsNullOrWhiteSpace(e.ErrorMessage)) - { - errors.Add(new ValidationError(e.ErrorMessage, m.Key)); - } - } - } - - throw new ValidationException("The model is not valid.", errors); - } - } + }+ public void OnException(ExceptionContext context) { From e1307d059d500e80403264f5dd0675c894e72fba Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 11:34:03 +0100 Subject: [PATCH 11/26] "+" removed. --- src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index c0248029e..42c2ba178 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -62,7 +62,7 @@ namespace Squidex.Pipeline error.StatusCode = statusCode; return new ObjectResult(error) { StatusCode = statusCode }; - }+ + } public void OnException(ExceptionContext context) { From 45350acaed1aa1281bfa0b6c6b5f26fb1f160ba0 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 11:49:03 +0100 Subject: [PATCH 12/26] Validation errors standardized. --- .../Scripting/JintScriptEngine.cs | 4 +- .../Apps/Guards/GuardAppClients.cs | 8 ++-- .../Assets/Guards/GuardAsset.cs | 2 +- .../Rules/Guards/GuardRule.cs | 6 +-- .../Rules/Guards/RuleActionValidator.cs | 18 ++++----- .../Guards/FieldPropertiesValidator.cs | 2 +- .../Schemas/Guards/GuardSchema.cs | 4 +- .../Schemas/Guards/GuardSchemaField.cs | 4 +- .../Controllers/Assets/AssetsController.cs | 4 +- .../Users/UserManagementController.cs | 1 + .../Pipeline/ApiModelValidationAttribute.cs | 37 +++++++++++++++++++ .../AssetsFieldPropertiesTests.cs | 4 +- 12 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 src/Squidex/Pipeline/ApiModelValidationAttribute.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index b9abcee99..57e135981 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -115,11 +115,11 @@ namespace Squidex.Domain.Apps.Core.Scripting } catch (ParserException ex) { - throw new ValidationException("Failed to execute script with javascript syntax error.", new ValidationError(ex.Message)); + throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); } catch (JavaScriptException ex) { - throw new ValidationException("Failed to execute script with javascript error.", new ValidationError(ex.Message)); + throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs index 604284090..b46cf9240 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { if (string.IsNullOrWhiteSpace(command.Id)) { - error(new ValidationError("Client id must be defined.", nameof(command.Id))); + error(new ValidationError("Client id is required.", nameof(command.Id))); } else if (clients.ContainsKey(command.Id)) { @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { if (string.IsNullOrWhiteSpace(command.Id)) { - error(new ValidationError("Client id must be defined.", nameof(command.Id))); + error(new ValidationError("Client id is required.", nameof(command.Id))); } }); } @@ -55,12 +55,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { if (string.IsNullOrWhiteSpace(command.Id)) { - error(new ValidationError("Client id must be defined.", nameof(command.Id))); + error(new ValidationError("Client id is required.", nameof(command.Id))); } if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null) { - error(new ValidationError("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission))); + error(new ValidationError("Either name or permission is required.", nameof(command.Name), nameof(command.Permission))); } if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue()) diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs index 756441da2..757959a6a 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { if (string.IsNullOrWhiteSpace(command.FileName)) { - error(new ValidationError("Name must be defined.", nameof(command.FileName))); + error(new ValidationError("Name is required.", nameof(command.FileName))); } if (string.Equals(command.FileName, oldName)) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs index 86707be2f..249553206 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards { if (command.Trigger == null) { - error(new ValidationError("Trigger must be defined.", nameof(command.Trigger))); + error(new ValidationError("Trigger is required.", nameof(command.Trigger))); } else { @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (command.Action == null) { - error(new ValidationError("Trigger must be defined.", nameof(command.Action))); + error(new ValidationError("Trigger is required.", nameof(command.Action))); } else { @@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards { if (command.Trigger == null && command.Action == null) { - error(new ValidationError("Either trigger or action must be defined.", nameof(command.Trigger), nameof(command.Action))); + error(new ValidationError("Either trigger or action is required.", nameof(command.Trigger), nameof(command.Action))); } if (command.Trigger != null) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs index 2d36a13c5..3b794922d 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs @@ -31,17 +31,17 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (string.IsNullOrWhiteSpace(action.ApiKey)) { - errors.Add(new ValidationError("Api key must be defined.", nameof(action.ApiKey))); + errors.Add(new ValidationError("Api key is required.", nameof(action.ApiKey))); } if (string.IsNullOrWhiteSpace(action.AppId)) { - errors.Add(new ValidationError("Application ID key must be defined.", nameof(action.AppId))); + errors.Add(new ValidationError("Application ID key is required.", nameof(action.AppId))); } if (string.IsNullOrWhiteSpace(action.IndexName)) { - errors.Add(new ValidationError("Index name must be defined.", nameof(action.ApiKey))); + errors.Add(new ValidationError("Index name is required.", nameof(action.IndexName))); } return Task.FromResult>(errors); @@ -53,12 +53,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (string.IsNullOrWhiteSpace(action.ConnectionString)) { - errors.Add(new ValidationError("Connection string must be defined.", nameof(action.ConnectionString))); + errors.Add(new ValidationError("Connection string is required.", nameof(action.ConnectionString))); } if (string.IsNullOrWhiteSpace(action.Queue)) { - errors.Add(new ValidationError("Queue must be defined.", nameof(action.Queue))); + errors.Add(new ValidationError("Queue is required.", nameof(action.Queue))); } else if (!Regex.IsMatch(action.Queue, "^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$")) { @@ -74,12 +74,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (string.IsNullOrWhiteSpace(action.ApiKey)) { - errors.Add(new ValidationError("Api key must be defined.", nameof(action.ApiKey))); + errors.Add(new ValidationError("Api key is required.", nameof(action.ApiKey))); } if (string.IsNullOrWhiteSpace(action.ServiceId)) { - errors.Add(new ValidationError("Service name must be defined.", nameof(action.ServiceId))); + errors.Add(new ValidationError("Service ID is required.", nameof(action.ServiceId))); } return Task.FromResult>(errors); @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (action.WebhookUrl == null || !action.WebhookUrl.IsAbsoluteUri) { - errors.Add(new ValidationError("Webhook Url must be specified and absolute.", nameof(action.WebhookUrl))); + errors.Add(new ValidationError("Webhook Url is required and must be an absolute URL.", nameof(action.WebhookUrl))); } return Task.FromResult>(errors); @@ -103,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (action.Url == null || !action.Url.IsAbsoluteUri) { - errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url))); + errors.Add(new ValidationError("Url is required and must be an absolute URL.", nameof(action.Url))); } return Task.FromResult>(errors); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs index 730a8083a..44becd941 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards if (properties.AspectWidth.HasValue != properties.AspectHeight.HasValue) { - yield return new ValidationError("Aspect width and height must be defined.", + yield return new ValidationError("Aspect width and height is required.", nameof(properties.AspectWidth), nameof(properties.AspectHeight)); } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index a7cd406c5..c95bac2f6 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards if (field.Properties == null) { - error(new ValidationError("Properties must be defined.", $"{prefix}.{nameof(field.Properties)}")); + error(new ValidationError("Properties is required.", $"{prefix}.{nameof(field.Properties)}")); } var propertyErrors = FieldPropertiesValidator.Validate(field.Properties); @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { if (command.FieldIds == null) { - error(new ValidationError("Field ids must be specified.", nameof(command.FieldIds))); + error(new ValidationError("Field ids is required.", nameof(command.FieldIds))); } if (command.FieldIds.Count != schema.Fields.Count || command.FieldIds.Any(x => !schema.FieldsById.ContainsKey(x))) diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs index 58ec7dc5b..73463a710 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards if (command.Properties == null) { - error(new ValidationError("Properties must be defined.", nameof(command.Properties))); + error(new ValidationError("Properties is required.", nameof(command.Properties))); } var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { if (command.Properties == null) { - error(new ValidationError("Properties must be defined.", nameof(command.Properties))); + error(new ValidationError("Properties is required.", nameof(command.Properties))); } var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 386e84edc..327a28e8f 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -146,7 +146,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// 400 => Asset exceeds the maximum size. /// /// - /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and must be defined correctly. + /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. /// [MustBeAppEditor] [HttpPost] @@ -248,7 +248,7 @@ namespace Squidex.Areas.Api.Controllers.Assets { if (file.Count != 1) { - var error = new ValidationError($"Can only upload one file, found {file.Count}."); + var error = new ValidationError($"Can only upload one file, found {file.Count} files."); throw new ValidationException("Cannot create asset.", error); } diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index 92fec91b3..d77397666 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -24,6 +24,7 @@ namespace Squidex.Areas.Api.Controllers.Users { [ApiAuthorize] [ApiExceptionFilter] + [ApiModelValidation] [MustBeAdministrator] [SwaggerIgnore] public sealed class UserManagementController : ApiController diff --git a/src/Squidex/Pipeline/ApiModelValidationAttribute.cs b/src/Squidex/Pipeline/ApiModelValidationAttribute.cs new file mode 100644 index 000000000..8b62de32f --- /dev/null +++ b/src/Squidex/Pipeline/ApiModelValidationAttribute.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Infrastructure; + +namespace Squidex.Pipeline +{ + public sealed class ApiModelValidationAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + var errors = new List(); + + foreach (var m in context.ModelState) + { + foreach (var e in m.Value.Errors) + { + if (!string.IsNullOrWhiteSpace(e.ErrorMessage)) + { + errors.Add(new ValidationError(e.ErrorMessage, m.Key)); + } + } + } + + throw new ValidationException("The model is not valid.", errors); + } + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs index 1c0f2c564..f031cd5eb 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties errors.ShouldBeEquivalentTo( new List { - new ValidationError("Aspect width and height must be defined.", "AspectWidth", "AspectHeight") + new ValidationError("Aspect width and height is required.", "AspectWidth", "AspectHeight") }); } @@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties errors.ShouldBeEquivalentTo( new List { - new ValidationError("Aspect width and height must be defined.", "AspectWidth", "AspectHeight") + new ValidationError("Aspect width and height is required.", "AspectWidth", "AspectHeight") }); } } From 50e338f434c925699c039c5f7095ad13065fc962 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 18:48:37 +0100 Subject: [PATCH 13/26] Cleaned up the commands and removed unnecessary properties. --- .../Actions/FastlyActionHandler.cs | 3 - .../Assets/MongoAssetEntity.cs | 15 +- .../MongoAssetRepository_SnapshotStore.cs | 1 + .../Assets/Visitors/FindExtensions.cs | 2 +- .../Contents/MongoContentEntity.cs | 20 +- .../Contents/MongoContentRepository.cs | 16 +- .../MongoContentRepository_SnapshotStore.cs | 8 +- .../Contents/Visitors/FindExtensions.cs | 2 +- .../History/MongoHistoryEventEntity.cs | 4 +- .../MongoRuleRepository_SnapshotStore.cs | 2 +- .../MongoSchemaRepository_SnapshotStore.cs | 2 +- .../AppProvider.cs | 2 +- .../Apps/AppCommandMiddleware.cs | 3 +- .../Apps/Commands/AddLanguage.cs | 2 +- .../Apps/Commands/AddPattern.cs | 2 +- .../Commands/AppCommand.cs} | 8 +- .../Apps/Commands/AssignContributor.cs | 2 +- .../Apps/Commands/AttachClient.cs | 9 +- .../Apps/Commands/ChangePlan.cs | 2 +- .../Apps/Commands/CreateApp.cs | 9 +- .../Apps/Commands/DeletePattern.cs | 2 +- .../Apps/Commands/RemoveContributor.cs | 2 +- .../Apps/Commands/RemoveLanguage.cs | 2 +- .../Apps/Commands/RevokeClient.cs | 2 +- .../Apps/Commands/UpdateClient.cs | 2 +- .../Apps/Commands/UpdateLanguage.cs | 2 +- .../Apps/Commands/UpdatePattern.cs | 2 +- .../Templates/CreateBlogCommandMiddleware.cs | 191 +++++++++--------- .../Assets/AssetDomainObject.cs | 11 + ...setAggregateCommand.cs => AssetCommand.cs} | 2 +- .../Assets/Commands/CreateAsset.cs | 5 +- .../Assets/Commands/DeleteAsset.cs | 2 +- .../Assets/Commands/RenameAsset.cs | 2 +- .../Assets/Commands/UpdateAsset.cs | 2 +- .../Assets/IAssetEntity.cs | 5 +- .../Assets/State/AssetState.cs | 8 +- .../Contents/Commands/ContentCommand.cs | 2 +- .../Contents/Commands/CreateContent.cs | 7 +- .../Commands/PublishContentAt.cs} | 6 +- .../Contents/ContentCommandMiddleware.cs | 10 + .../Contents/ContentDomainObject.cs | 25 +++ .../Contents/ContentEntity.cs | 8 +- .../Contents/ContentOperationContext.cs | 17 +- .../Contents/ContentPublisher.cs | 58 ++++++ .../GraphQL/Types/AppMutationsGraphType.cs | 44 ++-- .../Contents/Guards/GuardContent.cs | 14 ++ .../Contents/IContentEntity.cs | 12 +- .../Repositories/IContentRepository.cs | 3 + .../Contents/State/ContentState.cs | 30 ++- .../EntityMapper.cs | 9 - .../{AppCommand.cs => IAppCommand.cs} | 7 +- .../IEntityWithAppRef.cs | 16 -- .../{SchemaCommand.cs => ISchemaCommand.cs} | 7 +- .../Rules/Commands/CreateRule.cs | 5 +- .../Rules/Commands/DeleteRule.cs | 2 +- .../Rules/Commands/DisableRule.cs | 2 +- .../Rules/Commands/EnableRule.cs | 2 +- ...RuleAggregateCommand.cs => RuleCommand.cs} | 2 +- .../Rules/Commands/RuleEditCommand.cs | 2 +- .../Rules/Guards/GuardRule.cs | 5 +- .../Rules/IRuleEntity.cs | 5 +- .../Rules/RuleCommandMiddleware.cs | 2 +- .../Rules/RuleDomainObject.cs | 11 + .../Rules/State/RuleState.cs | 9 +- .../Schemas/Commands/AddField.cs | 2 +- .../Schemas/Commands/ConfigureScripts.cs | 2 +- .../Schemas/Commands/CreateSchema.cs | 15 +- .../Schemas/Commands/DeleteSchema.cs | 2 +- .../Schemas/Commands/FieldCommand.cs | 2 +- .../Schemas/Commands/PublishSchema.cs | 2 +- .../Schemas/Commands/ReorderFields.cs | 2 +- .../Commands/SchemaCommand.cs} | 6 +- .../Schemas/Commands/UnpublishSchema.cs | 2 +- .../Schemas/Commands/UpdateSchema.cs | 2 +- .../Schemas/ISchemaEntity.cs | 5 +- .../Schemas/SchemaDomainObject.cs | 16 ++ .../Schemas/State/SchemaState.cs | 12 +- .../Contents/ContentPublishScheduled.cs | 18 ++ .../Config/Authentication/MicrosoftHandler.cs | 1 - .../EnrichWithAppIdCommandMiddleware.cs | 2 +- .../EnrichWithSchemaIdCommandMiddleware.cs | 28 ++- .../Apps/AppCommandMiddlewareTests.cs | 3 + .../Apps/AppDomainObjectTests.cs | 7 + .../Assets/AssetDomainObjectTests.cs | 9 +- .../Contents/ContentDomainObjectTests.cs | 4 + .../Contents/GraphQL/GraphQLMutationTests.cs | 6 - .../Contents/TestData/FakeAssetEntity.cs | 4 +- .../Rules/Guards/GuardRuleTests.cs | 10 +- .../Rules/RuleDomainObjectTests.cs | 11 +- .../Schemas/SchemaDomainObjectTests.cs | 7 +- .../TestHelpers/HandlerTestBase.cs | 8 +- tools/Migrate_01/Migration02_AddPatterns.cs | 3 +- 92 files changed, 566 insertions(+), 303 deletions(-) rename src/Squidex.Domain.Apps.Entities/{SchemaAggregateCommand.cs => Apps/Commands/AppCommand.cs} (71%) rename src/Squidex.Domain.Apps.Entities/Assets/Commands/{AssetAggregateCommand.cs => AssetCommand.cs} (88%) rename src/Squidex.Domain.Apps.Entities/{IUpdateableEntityWithAppRef.cs => Contents/Commands/PublishContentAt.cs} (69%) create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs rename src/Squidex.Domain.Apps.Entities/{AppCommand.cs => IAppCommand.cs} (70%) delete mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs rename src/Squidex.Domain.Apps.Entities/{SchemaCommand.cs => ISchemaCommand.cs} (69%) rename src/Squidex.Domain.Apps.Entities/Rules/Commands/{RuleAggregateCommand.cs => RuleCommand.cs} (88%) rename src/Squidex.Domain.Apps.Entities/{AppAggregateCommand.cs => Schemas/Commands/SchemaCommand.cs} (77%) create mode 100644 src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs index bc3e716b3..5a8a252bd 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs @@ -8,14 +8,11 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Http; diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 468bd2110..2beb7522a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -20,9 +20,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets IAssetEntity, IUpdateableEntityWithVersion, IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy, - IUpdateableEntityWithAppRef + IUpdateableEntityWithLastModifiedBy { + [BsonRequired] + [BsonElement] + public Guid IdxAppId { get; set; } + + [BsonRequired] + [BsonElement] + public NamedId AppId { get; set; } + [BsonRequired] [BsonElement] public string MimeType { get; set; } @@ -55,10 +62,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement] public int? PixelHeight { get; set; } - [BsonRequired] - [BsonElement] - public Guid AppId { get; set; } - [BsonRequired] [BsonElement] public RefToken CreatedBy { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index 3e1cf0c22..2573debe2 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -36,6 +36,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets var entity = SimpleMapper.Map(value, new MongoAssetEntity()); entity.Version = newVersion; + entity.IdxAppId = value.AppId.Id; await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index b43fc8f48..f8ba52bf6 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors { var filters = new List> { - Filter.Eq(x => x.AppId, appId), + Filter.Eq(x => x.IdxAppId, appId), Filter.Eq(x => x.IsDeleted, false) }; diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 68d92be78..1cb2f8191 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -35,12 +35,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonRequired] [BsonElement("ai")] [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } + public Guid IdxAppId { get; set; } [BsonRequired] [BsonElement("si")] [BsonRepresentation(BsonType.String)] - public Guid SchemaId { get; set; } + public Guid IdxSchemaId { get; set; } [BsonRequired] [BsonElement("rf")] @@ -62,6 +62,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonJson] public IdContentData DataByIds { get; set; } + [BsonRequired] + [BsonElement("ai2")] + public NamedId AppId { get; set; } + + [BsonRequired] + [BsonElement("si2")] + public NamedId SchemaId { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("pa")] + public Instant? PublishAt { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("pb")] + public RefToken PublishAtBy { get; set; } + [BsonRequired] [BsonElement("ct")] public Instant Created { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index a8ad1abcc..ac05250e7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.OData.UriParser; using MongoDB.Driver; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; @@ -59,13 +60,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collection.Indexes.CreateOneAsync( Index .Text(x => x.DataText) - .Ascending(x => x.SchemaId) + .Ascending(x => x.IdxSchemaId) .Ascending(x => x.Status) .Ascending(x => x.IsDeleted)); await collection.Indexes.CreateOneAsync( Index - .Ascending(x => x.SchemaId) + .Ascending(x => x.IdxSchemaId) .Ascending(x => x.Id) .Ascending(x => x.IsDeleted) .Ascending(x => x.Status)); @@ -121,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { - var find = Collection.Find(x => x.SchemaId == schema.Id && ids.Contains(x.Id) && x.IsDeleted == false && status.Contains(x.Status)); + var find = Collection.Find(x => x.IdxSchemaId == schema.Id && ids.Contains(x.Id) && x.IsDeleted == false && status.Contains(x.Status)); var contentItems = find.ToListAsync(); var contentCount = find.CountAsync(); @@ -139,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList ids) { var contentEntities = - await Collection.Find(x => x.SchemaId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id) + await Collection.Find(x => x.IdxSchemaId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id) .ToListAsync(); return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList(); @@ -159,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) { var contentEntity = - await Collection.Find(x => x.SchemaId == schema.Id && x.Id == id && x.IsDeleted == false) + await Collection.Find(x => x.IdxSchemaId == schema.Id && x.Id == id && x.IsDeleted == false) .FirstOrDefaultAsync(); contentEntity?.ParseData(schema.SchemaDef); @@ -173,5 +174,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await base.ClearAsync(); } + + public Task QueryContentToPublishAsync(Instant now, Func callback) + { + throw new NotSupportedException(); + } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index c220e416e..7fa994d7f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents if (contentEntity != null) { - var schema = await GetSchemaAsync(contentEntity.AppId, contentEntity.SchemaId); + var schema = await GetSchemaAsync(contentEntity.IdxAppId, contentEntity.IdxSchemaId); contentEntity?.ParseData(schema.SchemaDef); @@ -40,12 +40,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) { - if (value.SchemaId == Guid.Empty) + if (value.SchemaId.Id == Guid.Empty) { return; } - var schema = await GetSchemaAsync(value.AppId, value.SchemaId); + var schema = await GetSchemaAsync(value.SchemaId.Id, value.SchemaId.Id); var idData = value.Data?.ToIdModel(schema.SchemaDef, true); @@ -53,6 +53,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents var document = SimpleMapper.Map(value, new MongoContentEntity { + IdxAppId = value.AppId.Id, + IdxSchemaId = value.SchemaId.Id, IsDeleted = value.IsDeleted, DocumentId = key.ToString(), DataText = idData?.ToFullText(), diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index c4ca769b6..8f9118e1c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { var filters = new List> { - Filter.Eq(x => x.SchemaId, schemaId), + Filter.Eq(x => x.IdxSchemaId, schemaId), Filter.In(x => x.Status, status), Filter.Eq(x => x.IsDeleted, false) }; diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs index 610d38977..5e9d9d50d 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs @@ -16,11 +16,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History { public sealed class MongoHistoryEventEntity : MongoEntity, IEntity, - IEntityWithAppRef, IUpdateableEntity, IUpdateableEntityWithVersion, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithAppRef + IUpdateableEntityWithCreatedBy { [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs index 408a3aaba..cd8a2ee02 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u .Set(x => x.State, value) - .Set(x => x.AppId, value.AppId) + .Set(x => x.AppId, value.AppId.Id) .Set(x => x.IsDeleted, value.IsDeleted)); } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs index aadd8cbcb..a23899a7a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u .Set(x => x.State, value) - .Set(x => x.AppId, value.AppId) + .Set(x => x.AppId, value.AppId.Id) .Set(x => x.Name, value.Name) .Set(x => x.IsDeleted, value.IsDeleted)); } diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index ace83d66c..ee7e5cab4 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities { var schema = await stateFactory.GetSingleAsync(id); - if (!IsFound(schema) || schema.Snapshot.IsDeleted || schema.Snapshot.AppId != appId) + if (!IsFound(schema) || schema.Snapshot.IsDeleted || schema.Snapshot.AppId.Id != appId) { return null; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 02bb242bd..829c1388c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Guards; @@ -180,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Apps } else { - var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, command.AppId.Id, a.Snapshot.Name, command.PlanId); + var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Snapshot.Id, a.Snapshot.Name, command.PlanId); if (result is PlanChangedResult) { diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs index b2619a6f6..3cfec0965 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AddLanguage : AppAggregateCommand + public sealed class AddLanguage : AppCommand { public Language Language { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs index 5442536c1..30873adbb 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs @@ -9,7 +9,7 @@ using System; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AddPattern : AppAggregateCommand + public sealed class AddPattern : AppCommand { public Guid PatternId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs similarity index 71% rename from src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs index d3d73dd62..a391da077 100644 --- a/src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs @@ -8,13 +8,15 @@ using System; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public abstract class SchemaAggregateCommand : SchemaCommand, IAggregateCommand + public abstract class AppCommand : SquidexCommand, IAggregateCommand { + public Guid AppId { get; set; } + Guid IAggregateCommand.AggregateId { - get { return SchemaId.Id; } + get { return AppId; } } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs index 13a60ddda..b54518a66 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AssignContributor : AppAggregateCommand + public sealed class AssignContributor : AppCommand { public string ContributorId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs index 4239c1e47..0fd8c6c4d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs @@ -9,10 +9,15 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AttachClient : AppAggregateCommand + public sealed class AttachClient : AppCommand { public string Id { get; set; } - public string Secret { get; } = RandomHash.New(); + public string Secret { get; set; } + + public AttachClient() + { + Secret = RandomHash.New(); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs index b5af896bf..a323b8b10 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class ChangePlan : AppAggregateCommand + public sealed class ChangePlan : AppCommand { public bool FromCallback { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index 5e8050247..d4dc2528b 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -10,19 +10,12 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class CreateApp : SquidexCommand, IAggregateCommand + public sealed class CreateApp : AppCommand, IAggregateCommand { - public Guid AppId { get; set; } - public string Name { get; set; } public string Template { get; set; } - Guid IAggregateCommand.AggregateId - { - get { return AppId; } - } - public CreateApp() { AppId = Guid.NewGuid(); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs index 5db33b435..199bff83c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs @@ -9,7 +9,7 @@ using System; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class DeletePattern : AppAggregateCommand + public sealed class DeletePattern : AppCommand { public Guid PatternId { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs index 6f707811f..a4e27d426 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class RemoveContributor : AppAggregateCommand + public sealed class RemoveContributor : AppCommand { public string ContributorId { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs index c863e7b85..602c35756 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class RemoveLanguage : AppAggregateCommand + public sealed class RemoveLanguage : AppCommand { public Language Language { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs index 623da3058..9361891ba 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class RevokeClient : AppAggregateCommand + public sealed class RevokeClient : AppCommand { public string Id { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs index 856de1f4a..6002bba30 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateClient : AppAggregateCommand + public sealed class UpdateClient : AppCommand { public string Id { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs index c0f442a25..52e40e5d0 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateLanguage : AppAggregateCommand + public sealed class UpdateLanguage : AppCommand { public Language Language { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs index 9ae7b510e..415856189 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs @@ -9,7 +9,7 @@ using System; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdatePattern : AppAggregateCommand + public sealed class UpdatePattern : AppCommand { public Guid PatternId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs index 6eeac4e55..adea21e4c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs @@ -36,17 +36,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates { var appId = new NamedId(createApp.AppId, createApp.Name); - Task publishAsync(AppCommand command) - { - command.AppId = appId; - - return context.CommandBus.PublishAsync(command); - } - return Task.WhenAll( - CreatePagesAsync(publishAsync, appId), - CreatePostsAsync(publishAsync, appId), - CreateClientAsync(publishAsync, appId)); + CreatePagesAsync(context.CommandBus, appId), + CreatePostsAsync(context.CommandBus, appId), + CreateClientAsync(context.CommandBus, appId)); } return TaskHelper.Done; @@ -57,16 +50,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase); } - private static async Task CreateClientAsync(Func publishAsync, NamedId appId) + private static async Task CreateClientAsync(ICommandBus bus, NamedId appId) { - await publishAsync(new AttachClient { Id = "sample-client" }); + await bus.PublishAsync(new AttachClient { Id = "sample-client" }); } - private async Task CreatePostsAsync(Func publishAsync, NamedId appId) + private async Task CreatePostsAsync(ICommandBus bus, NamedId appId) { - var postsId = await CreatePostsSchema(publishAsync); + var postsId = await CreatePostsSchema(bus, appId); - await publishAsync(new CreateContent + await bus.PublishAsync(new CreateContent { SchemaId = postsId, Data = @@ -81,11 +74,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates }); } - private async Task CreatePagesAsync(Func publishAsync, NamedId appId) + private async Task CreatePagesAsync(ICommandBus bus, NamedId appId) { - var pagesId = await CreatePagesSchema(publishAsync); + var pagesId = await CreatePagesSchema(bus, appId); - await publishAsync(new CreateContent + await bus.PublishAsync(new CreateContent { SchemaId = pagesId, Data = @@ -100,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates }); } - private async Task> CreatePostsSchema(Func publishAsync) + private async Task> CreatePostsSchema(ICommandBus bus, NamedId appId) { var command = new CreateSchema { @@ -111,58 +104,59 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates Label = "Posts" }, Fields = new List + { + new CreateSchemaField { - new CreateSchemaField + Name = "title", + Partitioning = Partitioning.Invariant.Key, + Properties = new StringFieldProperties { - Name = "title", - Partitioning = Partitioning.Invariant.Key, - Properties = new StringFieldProperties - { - Editor = StringFieldEditor.Input, - IsRequired = true, - IsListField = true, - MaxLength = 100, - MinLength = 0, - Label = "Title" - } - }, - new CreateSchemaField + Editor = StringFieldEditor.Input, + IsRequired = true, + IsListField = true, + MaxLength = 100, + MinLength = 0, + Label = "Title" + } + }, + new CreateSchemaField + { + Name = "slug", + Partitioning = Partitioning.Invariant.Key, + Properties = new StringFieldProperties { - Name = "slug", - Partitioning = Partitioning.Invariant.Key, - Properties = new StringFieldProperties - { - Editor = StringFieldEditor.Slug, - IsRequired = false, - IsListField = true, - MaxLength = 100, - MinLength = 0, - Label = "Slug (Autogenerated)" - }, - IsDisabled = true + Editor = StringFieldEditor.Slug, + IsRequired = false, + IsListField = true, + MaxLength = 100, + MinLength = 0, + Label = "Slug (Autogenerated)" }, - new CreateSchemaField + IsDisabled = true + }, + new CreateSchemaField + { + Name = "text", + Partitioning = Partitioning.Invariant.Key, + Properties = new StringFieldProperties { - Name = "text", - Partitioning = Partitioning.Invariant.Key, - Properties = new StringFieldProperties - { - Editor = StringFieldEditor.RichText, - IsRequired = true, - IsListField = false, - Label = "Text" - } + Editor = StringFieldEditor.RichText, + IsRequired = true, + IsListField = false, + Label = "Text" } } + }, + AppId = appId }; - await publishAsync(command); + await bus.PublishAsync(command); var schemaId = new NamedId(command.SchemaId, command.Name); - await publishAsync(new ConfigureScripts + await bus.PublishAsync(new ConfigureScripts { - SchemaId = schemaId, + SchemaId = schemaId.Id, ScriptCreate = SlugScript, ScriptUpdate = SlugScript }); @@ -170,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates return schemaId; } - private async Task> CreatePagesSchema(Func publishAsync) + private async Task> CreatePagesSchema(ICommandBus bus, NamedId appId) { var command = new CreateSchema { @@ -180,58 +174,59 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates Label = "Pages" }, Fields = new List + { + new CreateSchemaField { - new CreateSchemaField + Name = "title", + Partitioning = Partitioning.Invariant.Key, + Properties = new StringFieldProperties { - Name = "title", - Partitioning = Partitioning.Invariant.Key, - Properties = new StringFieldProperties - { - Editor = StringFieldEditor.Input, - IsRequired = true, - IsListField = true, - MaxLength = 100, - MinLength = 0, - Label = "Title" - } - }, - new CreateSchemaField + Editor = StringFieldEditor.Input, + IsRequired = true, + IsListField = true, + MaxLength = 100, + MinLength = 0, + Label = "Title" + } + }, + new CreateSchemaField + { + Name = "slug", + Partitioning = Partitioning.Invariant.Key, + Properties = new StringFieldProperties { - Name = "slug", - Partitioning = Partitioning.Invariant.Key, - Properties = new StringFieldProperties - { - Editor = StringFieldEditor.Slug, - IsRequired = false, - IsListField = true, - MaxLength = 100, - MinLength = 0, - Label = "Slug (Autogenerated)" - }, - IsDisabled = true + Editor = StringFieldEditor.Slug, + IsRequired = false, + IsListField = true, + MaxLength = 100, + MinLength = 0, + Label = "Slug (Autogenerated)" }, - new CreateSchemaField + IsDisabled = true + }, + new CreateSchemaField + { + Name = "text", + Partitioning = Partitioning.Invariant.Key, + Properties = new StringFieldProperties { - Name = "text", - Partitioning = Partitioning.Invariant.Key, - Properties = new StringFieldProperties - { - Editor = StringFieldEditor.RichText, - IsRequired = true, - IsListField = false, - Label = "Text" - } + Editor = StringFieldEditor.RichText, + IsRequired = true, + IsListField = false, + Label = "Text" } } + }, + AppId = appId }; - await publishAsync(command); + await bus.PublishAsync(command); var schemaId = new NamedId(command.SchemaId, command.Name); - await publishAsync(new ConfigureScripts + await bus.PublishAsync(new ConfigureScripts { - SchemaId = schemaId, + SchemaId = schemaId.Id, ScriptCreate = SlugScript, ScriptUpdate = SlugScript }); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index 103cd9981..cd8e74dd4 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -7,6 +7,7 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -74,6 +75,16 @@ namespace Squidex.Domain.Apps.Entities.Assets return this; } + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + private void VerifyNotCreated() { if (!string.IsNullOrWhiteSpace(Snapshot.FileName)) diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs similarity index 88% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs rename to src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs index efdb41e00..4898243dd 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public abstract class AssetAggregateCommand : AppCommand, IAggregateCommand + public abstract class AssetCommand : SquidexCommand, IAggregateCommand { public Guid AssetId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index f7696e14b..f421d9ed8 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -6,12 +6,15 @@ // ========================================================================== using System; +using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public sealed class CreateAsset : AssetAggregateCommand + public sealed class CreateAsset : AssetCommand, IAppCommand { + public NamedId AppId { get; set; } + public AssetFile File { get; set; } public ImageInfo ImageInfo { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs index 351333962..4848be209 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public sealed class DeleteAsset : AssetAggregateCommand + public sealed class DeleteAsset : AssetCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs index 3dc784b88..65cba7f35 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public sealed class RenameAsset : AssetAggregateCommand + public sealed class RenameAsset : AssetCommand { public string FileName { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs index 8c419ca71..1bb193419 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure.Assets; namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public sealed class UpdateAsset : AssetAggregateCommand + public sealed class UpdateAsset : AssetCommand { public AssetFile File { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs index c95179f66..c61c52cc0 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs @@ -5,18 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetEntity : IEntity, - IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion, IAssetInfo { + NamedId AppId { get; } + string MimeType { get; } long FileVersion { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index 8a2f65796..3ed7714a7 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; @@ -18,11 +19,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.State { public class AssetState : DomainObjectState, IAssetEntity, - IAssetInfo, - IUpdateableEntityWithAppRef + IAssetInfo { [JsonProperty] - public Guid AppId { get; set; } + public NamedId AppId { get; set; } [JsonProperty] public string FileName { get; set; } @@ -61,6 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.State SimpleMapper.Map(@event, this); TotalSize += @event.FileSize; + + AppId = @event.AppId; } protected void On(AssetUpdated @event) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs index 3629bdfdc..8e15d2a7b 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public abstract class ContentCommand : SchemaCommand, IAggregateCommand + public abstract class ContentCommand : SquidexCommand, IAggregateCommand { public Guid ContentId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs index 71e0a11e7..2eec65f73 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs @@ -6,11 +6,16 @@ // ========================================================================== using System; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public sealed class CreateContent : ContentDataCommand + public sealed class CreateContent : ContentDataCommand, ISchemaCommand, IAppCommand { + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } + public bool Publish { get; set; } public CreateContent() diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs similarity index 69% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs rename to src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs index c8ca9da7e..3b20c1aeb 100644 --- a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs @@ -7,10 +7,10 @@ using System; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public interface IUpdateableEntityWithAppRef + public sealed class PublishContentAt : ContentDataCommand { - Guid AppId { get; set; } + public DateTimeOffset PublishAt { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs index 6b10ba6df..dec465b99 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -131,6 +131,16 @@ namespace Squidex.Domain.Apps.Entities.Contents }); } + protected Task On(PublishContentAt command, CommandContext context) + { + return handler.UpdateAsync(context, content => + { + GuardContent.CanPublishAt(command); + + content.PublishAt(command); + }); + } + public async Task HandleAsync(CommandContext context, Func next) { await this.DispatchActionAsync(context.Command, context); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index 0b46b12d7..dd7e4fc05 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -8,6 +8,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -50,6 +51,15 @@ namespace Squidex.Domain.Apps.Entities.Contents return this; } + public ContentDomainObject PublishAt(PublishContentAt command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentPublishScheduled())); + + return this; + } + public ContentDomainObject Update(UpdateContent command) { VerifyCreatedAndNotDeleted(); @@ -80,6 +90,21 @@ namespace Squidex.Domain.Apps.Entities.Contents return this; } + private void RaiseEvent(SchemaEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + if (@event.SchemaId == null) + { + @event.SchemaId = Snapshot.SchemaId; + } + + RaiseEvent(Envelope.Create(@event)); + } + private void VerifyNotCreated() { if (Snapshot.Data != null) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 9c0bc7af7..84269d0e5 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -18,7 +18,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { public Guid Id { get; set; } - public Guid AppId { get; set; } + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } public long Version { get; set; } @@ -26,6 +28,10 @@ namespace Squidex.Domain.Apps.Entities.Contents public Instant LastModified { get; set; } + public Instant? PublishAt { get; set; } + + public RefToken PublishAtBy { get; set; } + public RefToken CreatedBy { get; set; } public RefToken LastModifiedBy { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index c6a8c72b3..8660af246 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -42,7 +42,16 @@ namespace Squidex.Domain.Apps.Entities.Contents IScriptEngine scriptEngine, Func message) { - var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id); + var a = content.Snapshot.AppId; + var s = content.Snapshot.SchemaId; + + if (command is CreateContent createContent) + { + a = a ?? createContent.AppId; + s = s ?? createContent.SchemaId; + } + + var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(a.Id, s.Id); var context = new ContentOperationContext { @@ -75,17 +84,15 @@ namespace Squidex.Domain.Apps.Entities.Contents { var errors = new List(); - var appId = command.AppId.Id; - var ctx = new ValidationContext( (contentIds, schemaId) => { - return QueryContentsAsync(appId, schemaId, contentIds); + return QueryContentsAsync(content.Snapshot.AppId.Id, schemaId, contentIds); }, assetIds => { - return QueryAssetsAsync(appId, assetIds); + return QueryAssetsAsync(content.Snapshot.AppId.Id, assetIds); }); if (partial) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs new file mode 100644 index 000000000..026a53296 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentPublisher : IRunnable + { + private readonly CompletionTimer timer; + private readonly IContentRepository contentRepository; + private readonly ICommandBus commandBus; + private readonly IClock clock; + + public ContentPublisher( + IContentRepository contentRepository, + ICommandBus commandBus, + IClock clock) + { + Guard.NotNull(contentRepository, nameof(contentRepository)); + Guard.NotNull(commandBus, nameof(commandBus)); + Guard.NotNull(clock, nameof(clock)); + + this.contentRepository = contentRepository; + this.commandBus = commandBus; + this.clock = clock; + + timer = new CompletionTimer(5000, x => PublishAsync()); + } + + public void Run() + { + } + + private Task PublishAsync() + { + var now = clock.GetCurrentInstant(); + + return contentRepository.QueryContentToPublishAsync(now, content => + { + var command = new ChangeContentStatus { ContentId = content.Id, Status = Status.Published, Actor = content.PublishAtBy }; + + return commandBus.PublishAsync(command); + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs index 22c30512c..58ec5d018 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs @@ -38,13 +38,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types var inputType = new ContentDataGraphInputType(model, schema); AddContentCreate(schemaId, schemaType, schemaName, inputType, contentDataType, contentType); - AddContentUpdate(schemaId, schemaType, schemaName, inputType, resultType); - AddContentPatch(schemaId, schemaType, schemaName, inputType, resultType); - AddContentPublish(schemaId, schemaType, schemaName); - AddContentUnpublish(schemaId, schemaType, schemaName); - AddContentArchive(schemaId, schemaType, schemaName); - AddContentRestore(schemaId, schemaType, schemaName); - AddContentDelete(schemaId, schemaType, schemaName); + AddContentUpdate(schemaType, schemaName, inputType, resultType); + AddContentPatch(schemaType, schemaName, inputType, resultType); + AddContentPublish(schemaType, schemaName); + AddContentUnpublish(schemaType, schemaName); + AddContentArchive(schemaType, schemaName); + AddContentRestore(schemaType, schemaName); + AddContentDelete(schemaType, schemaName); } Description = "The app mutations."; @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types var contentData = GetContentData(c); - var command = new CreateContent { SchemaId = schemaId, ContentId = Guid.NewGuid(), Data = contentData, Publish = argPublish }; + var command = new CreateContent { SchemaId = schemaId, Data = contentData, Publish = argPublish }; var commandContext = await publish(command); var result = commandContext.Result>(); @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentUpdate(NamedId schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType resultType) + private void AddContentUpdate(string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType resultType) { AddField(new FieldType { @@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types var contentId = c.GetArgument("id"); var contentData = GetContentData(c); - var command = new UpdateContent { SchemaId = schemaId, ContentId = contentId, Data = contentData }; + var command = new UpdateContent { ContentId = contentId, Data = contentData }; var commandContext = await publish(command); var result = commandContext.Result(); @@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentPatch(NamedId schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType resultType) + private void AddContentPatch(string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType resultType) { AddField(new FieldType { @@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types var contentId = c.GetArgument("id"); var contentData = GetContentData(c); - var command = new PatchContent { SchemaId = schemaId, ContentId = contentId, Data = contentData }; + var command = new PatchContent { ContentId = contentId, Data = contentData }; var commandContext = await publish(command); var result = commandContext.Result(); @@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentPublish(NamedId schemaId, string schemaType, string schemaName) + private void AddContentPublish(string schemaType, string schemaName) { AddField(new FieldType { @@ -201,7 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var contentId = c.GetArgument("id"); - var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Published }; + var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Published }; return publish(command); }), @@ -209,7 +209,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentUnpublish(NamedId schemaId, string schemaType, string schemaName) + private void AddContentUnpublish(string schemaType, string schemaName) { AddField(new FieldType { @@ -220,7 +220,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var contentId = c.GetArgument("id"); - var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Draft }; + var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft }; return publish(command); }), @@ -228,7 +228,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentArchive(NamedId schemaId, string schemaType, string schemaName) + private void AddContentArchive(string schemaType, string schemaName) { AddField(new FieldType { @@ -239,7 +239,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var contentId = c.GetArgument("id"); - var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Archived }; + var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Archived }; return publish(command); }), @@ -247,7 +247,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentRestore(NamedId schemaId, string schemaType, string schemaName) + private void AddContentRestore(string schemaType, string schemaName) { AddField(new FieldType { @@ -258,7 +258,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var contentId = c.GetArgument("id"); - var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Draft }; + var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft }; return publish(command); }), @@ -266,7 +266,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentDelete(NamedId schemaId, string schemaType, string schemaName) + private void AddContentDelete(string schemaType, string schemaName) { AddField(new FieldType { @@ -277,7 +277,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var contentId = c.GetArgument("id"); - var command = new DeleteContent { SchemaId = schemaId, ContentId = contentId }; + var command = new DeleteContent { ContentId = contentId }; return publish(command); }), diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 1ae0abb79..092c144df 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; @@ -65,6 +66,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards }); } + public static void CanPublishAt(PublishContentAt command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot schedule content tol publish.", error => + { + if (command.PublishAt < DateTime.UtcNow) + { + error(new ValidationError("Date must be in the future.", nameof(command.PublishAt))); + } + }); + } + public static void CanDelete(DeleteContent command) { Guard.NotNull(command, nameof(command)); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs index 4e9573116..d17ccc2e7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -6,19 +6,29 @@ // ========================================================================== // ========================================================================== +using System; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentEntity : IEntity, - IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { + NamedId AppId { get; } + + NamedId SchemaId { get; } + Status Status { get; } + Instant? PublishAt { get; } + + RefToken PublishAtBy { get; } + NamedContentData Data { get; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 509fc42ca..3f925d049 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.OData.UriParser; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; @@ -27,5 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version); + + Task QueryContentToPublishAsync(Instant now, Func callback); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index cdc9884f1..75bb8f6d2 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -7,38 +7,47 @@ using System; using Newtonsoft.Json; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Contents.State { public class ContentState : DomainObjectState, - IContentEntity, - IUpdateableEntityWithAppRef + IContentEntity { [JsonProperty] - public NamedContentData Data { get; set; } + public NamedId AppId { get; set; } [JsonProperty] - public Guid AppId { get; set; } + public NamedId SchemaId { get; set; } [JsonProperty] - public Guid SchemaId { get; set; } + public NamedContentData Data { get; set; } [JsonProperty] public Status Status { get; set; } + [JsonProperty] + public RefToken PublishAtBy { get; set; } + + [JsonProperty] + public Instant? PublishAt { get; set; } + [JsonProperty] public bool IsDeleted { get; set; } protected void On(ContentCreated @event) { - SchemaId = @event.SchemaId.Id; + SchemaId = @event.SchemaId; Data = @event.Data; + + AppId = @event.AppId; } protected void On(ContentUpdated @event) @@ -46,9 +55,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.State Data = @event.Data; } + protected void On(ContentPublishScheduled @event) + { + PublishAt = @event.PublishAt; + PublishAtBy = @event.Actor; + } + protected void On(ContentStatusChanged @event) { Status = @event.Status; + + PublishAt = null; + PublishAtBy = null; } protected void On(ContentDeleted @event) diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index 1dd17d847..f990ae781 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -17,7 +17,6 @@ namespace Squidex.Domain.Apps.Entities public static T Update(this T entity, SquidexEvent @event, EnvelopeHeaders headers, Action updater = null) where T : IEntity { SetId(entity, headers); - SetAppId(entity, @event); SetCreated(entity, headers); SetCreatedBy(entity, @event); SetLastModified(entity, headers); @@ -76,13 +75,5 @@ namespace Squidex.Domain.Apps.Entities withModifiedBy.LastModifiedBy = @event.Actor; } } - - private static void SetAppId(IEntity entity, SquidexEvent @event) - { - if (entity is IUpdateableEntityWithAppRef appEntity && @event is AppEvent appEvent) - { - appEntity.AppId = appEvent.AppId.Id; - } - } } } diff --git a/src/Squidex.Domain.Apps.Entities/AppCommand.cs b/src/Squidex.Domain.Apps.Entities/IAppCommand.cs similarity index 70% rename from src/Squidex.Domain.Apps.Entities/AppCommand.cs rename to src/Squidex.Domain.Apps.Entities/IAppCommand.cs index ba5b6a4e4..6a7bcf31b 100644 --- a/src/Squidex.Domain.Apps.Entities/AppCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/IAppCommand.cs @@ -1,17 +1,18 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities { - public abstract class AppCommand : SquidexCommand + public interface IAppCommand : ICommand { - public NamedId AppId { get; set; } + NamedId AppId { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs deleted file mode 100644 index bf899acef..000000000 --- a/src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Entities -{ - public interface IEntityWithAppRef - { - Guid AppId { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/SchemaCommand.cs b/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs similarity index 69% rename from src/Squidex.Domain.Apps.Entities/SchemaCommand.cs rename to src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs index 62db6405d..bd75842d8 100644 --- a/src/Squidex.Domain.Apps.Entities/SchemaCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs @@ -1,17 +1,18 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities { - public abstract class SchemaCommand : AppCommand + public interface ISchemaCommand : ICommand { - public NamedId SchemaId { get; set; } + NamedId SchemaId { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs index 667f95dd7..07b49c12b 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs @@ -6,11 +6,14 @@ // ========================================================================== using System; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Rules.Commands { - public sealed class CreateRule : RuleEditCommand + public sealed class CreateRule : RuleEditCommand, IAppCommand { + public NamedId AppId { get; set; } + public CreateRule() { RuleId = Guid.NewGuid(); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs index 055730bd9..d895ed5b4 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Commands { - public sealed class DeleteRule : RuleAggregateCommand + public sealed class DeleteRule : RuleCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs index 0718a7b12..de40cf95a 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Commands { - public sealed class DisableRule : RuleAggregateCommand + public sealed class DisableRule : RuleCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs index b35f97d86..62cd528f0 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Commands { - public sealed class EnableRule : RuleAggregateCommand + public sealed class EnableRule : RuleCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs similarity index 88% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs rename to src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs index 41a81ea3e..7d8690c46 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Rules.Commands { - public abstract class RuleAggregateCommand : AppCommand, IAggregateCommand + public abstract class RuleCommand : SquidexCommand, IAggregateCommand { public Guid RuleId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs index ba8d77981..e461ff8ac 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Domain.Apps.Entities.Rules.Commands { - public abstract class RuleEditCommand : RuleAggregateCommand + public abstract class RuleEditCommand : RuleCommand { public RuleTrigger Trigger { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs index 249553206..77f1298df 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; @@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards }); } - public static Task CanUpdate(UpdateRule command, IAppProvider appProvider) + public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider) { Guard.NotNull(command, nameof(command)); @@ -57,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (command.Trigger != null) { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, command.Trigger, appProvider); errors.Foreach(error); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs index 2caaa814c..ef69c574f 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs @@ -5,17 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Rules { public interface IRuleEntity : IEntity, - IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { + NamedId AppId { get; set; } + Rule RuleDef { get; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index c0ee1cf5b..8a6e43441 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Rules { return handler.UpdateSyncedAsync(context, async r => { - await GuardRule.CanUpdate(command, appProvider); + await GuardRule.CanUpdate(command, r.Snapshot.AppId.Id, appProvider); r.Update(command); }); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index bc05ac8bd..03d3dd802 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -7,6 +7,7 @@ using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -52,6 +53,16 @@ namespace Squidex.Domain.Apps.Entities.Rules RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); } + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + private void VerifyNotCreated() { if (Snapshot.RuleDef != null) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index 7b6dd9602..fa87078da 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -10,18 +10,17 @@ using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Rules.State { public class RuleState : DomainObjectState, - IRuleEntity, - IEntityWithAppRef, - IUpdateableEntityWithAppRef + IRuleEntity { [JsonProperty] - public Guid AppId { get; set; } + public NamedId AppId { get; set; } [JsonProperty] public Rule RuleDef { get; set; } @@ -32,6 +31,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.State protected void On(RuleCreated @event) { RuleDef = new Rule(@event.Trigger, @event.Action); + + AppId = @event.AppId; } protected void On(RuleUpdated @event) diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs index e14d082e8..8856821d6 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class AddField : SchemaAggregateCommand + public sealed class AddField : SchemaCommand { public string Name { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs index f4fea680a..d850076a6 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class ConfigureScripts : SchemaAggregateCommand + public sealed class ConfigureScripts : SchemaCommand { public string ScriptQuery { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs index afbd735f5..923499e79 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -7,14 +7,16 @@ using System; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure; using SchemaFields = System.Collections.Generic.List; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class CreateSchema : AppCommand, IAggregateCommand + public sealed class CreateSchema : SchemaCommand, IAppCommand { - public Guid SchemaId { get; set; } + public NamedId AppId { get; set; } + + public string Name { get; set; } public SchemaFields Fields { get; set; } @@ -22,13 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public bool Publish { get; set; } - public string Name { get; set; } - - Guid IAggregateCommand.AggregateId - { - get { return SchemaId; } - } - public CreateSchema() { SchemaId = Guid.NewGuid(); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs index d4c3d9bfb..d3b79c454 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class DeleteSchema : SchemaAggregateCommand + public sealed class DeleteSchema : SchemaCommand { } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs index 9ddbd0301..5ad93ddf1 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public class FieldCommand : SchemaAggregateCommand + public class FieldCommand : SchemaCommand { public long FieldId { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs index 8bb789b72..c8d68314d 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class PublishSchema : SchemaAggregateCommand + public sealed class PublishSchema : SchemaCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs index 068b41162..9afe0346c 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class ReorderFields : SchemaAggregateCommand + public sealed class ReorderFields : SchemaCommand { public List FieldIds { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs similarity index 77% rename from src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs rename to src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs index a4143215c..49bba3620 100644 --- a/src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs @@ -10,11 +10,13 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities { - public class AppAggregateCommand : AppCommand, IAggregateCommand + public abstract class SchemaCommand : SquidexCommand, IAggregateCommand { + public Guid SchemaId { get; set; } + Guid IAggregateCommand.AggregateId { - get { return AppId.Id; } + get { return SchemaId; } } } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs index c8c2b722d..31d5c284a 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class UnpublishSchema : SchemaAggregateCommand + public sealed class UnpublishSchema : SchemaCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs index 329cbc400..579f55bb7 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class UpdateSchema : SchemaAggregateCommand + public sealed class UpdateSchema : SchemaCommand { public SchemaProperties Properties { get; set; } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs index da1fd2045..8c341e76e 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs @@ -5,17 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Schemas { public interface ISchemaEntity : IEntity, - IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { + NamedId AppId { get; } + string Name { get; } bool IsPublished { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index cc081ee28..04c904853 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -190,6 +191,21 @@ namespace Squidex.Domain.Apps.Entities.Schemas RaiseEvent(@event); } + private void RaiseEvent(SchemaEvent @event) + { + if (@event.SchemaId == null) + { + @event.SchemaId = new NamedId(Snapshot.Id, Snapshot.Name); + } + + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + private void VerifyNotCreated() { if (Snapshot.SchemaDef != null) diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 5d06deb3b..bc148430b 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; @@ -18,16 +19,13 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Schemas.State { public class SchemaState : DomainObjectState, - ISchemaEntity, - IUpdateableEntityWithAppRef, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy + ISchemaEntity { [JsonProperty] - public string Name { get; set; } + public NamedId AppId { get; set; } [JsonProperty] - public Guid AppId { get; set; } + public string Name { get; set; } [JsonProperty] public int TotalFields { get; set; } = 0; @@ -108,6 +106,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State } SchemaDef = schema; + + AppId = @event.AppId; } protected void On(FieldAdded @event, FieldRegistry registry) diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs new file mode 100644 index 000000000..50ef1581f --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Contents +{ + [EventType(nameof(ContentPublishScheduled))] + public sealed class ContentPublishScheduled : ContentEvent + { + public Instant PublishAt { get; set; } + } +} diff --git a/src/Squidex/Config/Authentication/MicrosoftHandler.cs b/src/Squidex/Config/Authentication/MicrosoftHandler.cs index c308814af..168995ad9 100644 --- a/src/Squidex/Config/Authentication/MicrosoftHandler.cs +++ b/src/Squidex/Config/Authentication/MicrosoftHandler.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; using Squidex.Shared.Identity; diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs index 27d93b073..908fee35d 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs @@ -25,7 +25,7 @@ namespace Squidex.Pipeline.CommandMiddlewares public Task HandleAsync(CommandContext context, Func next) { - if (context.Command is AppCommand appCommand && appCommand.AppId == null) + if (context.Command is IAppCommand appCommand && appCommand.AppId == null) { var appFeature = httpContextAccessor.HttpContext.Features.Get(); diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs index 15a20857f..de1552cf8 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -29,8 +29,30 @@ namespace Squidex.Pipeline.CommandMiddlewares public async Task HandleAsync(CommandContext context, Func next) { - if (context.Command is SchemaCommand schemaCommand && schemaCommand.SchemaId == null) + if (context.Command is ISchemaCommand schemaCommand && schemaCommand.SchemaId == null) { + NamedId appId = null; + + if (context.Command is IAppCommand appCommand) + { + appId = appCommand.AppId; + } + + if (appId == null) + { + var appFeature = actionContextAccessor.ActionContext.HttpContext.Features.Get(); + + if (appFeature != null && appFeature.App != null) + { + appId = new NamedId(appFeature.App.Id, appFeature.App.Name); + } + } + + if (appId == null) + { + return; + } + var routeValues = actionContextAccessor.ActionContext.RouteData.Values; if (routeValues.ContainsKey("name")) @@ -41,11 +63,11 @@ namespace Squidex.Pipeline.CommandMiddlewares if (Guid.TryParse(schemaName, out var id)) { - schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, id); + schema = await appProvider.GetSchemaAsync(appId.Id, id); } else { - schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, schemaName); + schema = await appProvider.GetSchemaAsync(appId.Id, schemaName); } if (schema == null) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs index 42223eaaa..18ab306fa 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; using Squidex.Shared.Users; using Xunit; @@ -46,6 +47,8 @@ namespace Squidex.Domain.Apps.Entities.Apps .Returns(A.Fake()); sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver); + + app.ActivateAsync(Id, A.Fake>()); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index 5e6a61a90..e49cf171e 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -8,11 +8,13 @@ using System; using System.Collections.Generic; using System.Linq; +using FakeItEasy; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.States; using Xunit; namespace Squidex.Domain.Apps.Entities.Apps @@ -31,6 +33,11 @@ namespace Squidex.Domain.Apps.Entities.Apps get { return AppId; } } + public AppDomainObjectTests() + { + sut.ActivateAsync(Id, A.Fake>()); + } + [Fact] public void Create_should_throw_exception_if_created() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs index 4f2d8400d..b1c057749 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs @@ -7,11 +7,13 @@ using System; using System.IO; +using FakeItEasy; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.States; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets @@ -28,6 +30,11 @@ namespace Squidex.Domain.Apps.Entities.Assets get { return assetId; } } + public AssetDomainObjectTests() + { + sut.ActivateAsync(Id, A.Fake>()); + } + [Fact] public void Create_should_throw_exception_if_created() { @@ -203,7 +210,7 @@ namespace Squidex.Domain.Apps.Entities.Assets return CreateEvent(@event); } - protected T CreateAssetCommand(T command) where T : AssetAggregateCommand + protected T CreateAssetCommand(T command) where T : AssetCommand { command.AssetId = assetId; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index f3f731edf..b0a061422 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -6,12 +6,14 @@ // ========================================================================== using System; +using FakeItEasy; using FluentAssertions; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.States; using Xunit; namespace Squidex.Domain.Apps.Entities.Contents @@ -40,6 +42,8 @@ namespace Squidex.Domain.Apps.Entities.Contents public ContentDomainObjectTests() { patched = otherData.MergeInto(data); + + sut.ActivateAsync(Id, A.Fake>()); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index d92690828..aeebd190d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -11,7 +11,6 @@ using FakeItEasy; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Xunit; @@ -328,7 +327,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schema.NamedId()) && x.ContentId == contentId && x.Status == Status.Published && x.ExpectedVersion == 10))) @@ -364,7 +362,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schema.NamedId()) && x.ContentId == contentId && x.Status == Status.Draft && x.ExpectedVersion == 10))) @@ -400,7 +397,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schema.NamedId()) && x.ContentId == contentId && x.Status == Status.Archived && x.ExpectedVersion == 10))) @@ -436,7 +432,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schema.NamedId()) && x.ContentId == contentId && x.Status == Status.Draft && x.ExpectedVersion == 10))) @@ -472,7 +467,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => - x.SchemaId.Equals(schema.NamedId()) && x.ContentId == contentId && x.ExpectedVersion == 10))) .MustHaveHappened(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs index c8bf013c3..82a77eaef 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs @@ -14,9 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData { public sealed class FakeAssetEntity : IAssetEntity { - public Guid Id { get; set; } + public NamedId AppId { get; set; } - public Guid AppId { get; set; } + public Guid Id { get; set; } public Guid AssetId { get; set; } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs index 65607a265..9d061491c 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -87,13 +87,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards { var command = new UpdateRule(); - await Assert.ThrowsAsync(() => GuardRule.CanUpdate(command, appProvider)); + await Assert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider)); } [Fact] public async Task CanUpdate_should_not_throw_exception_if_trigger_and_action_valid() { - var command = CreateCommand(new UpdateRule + var command = new UpdateRule { Trigger = new ContentChangedTrigger { @@ -103,9 +103,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards { Url = validUrl } - }); + }; - await GuardRule.CanUpdate(command, appProvider); + await GuardRule.CanUpdate(command, appId.Id, appProvider); } [Fact] @@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards GuardRule.CanDelete(command); } - private T CreateCommand(T command) where T : AppCommand + private CreateRule CreateCommand(CreateRule command) { command.AppId = appId; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs index 095e5b439..0afbaab17 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Immutable; +using FakeItEasy; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -14,6 +15,7 @@ using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; +using Squidex.Infrastructure.States; using Xunit; namespace Squidex.Domain.Apps.Entities.Rules @@ -30,6 +32,11 @@ namespace Squidex.Domain.Apps.Entities.Rules get { return ruleId; } } + public RuleDomainObjectTests() + { + sut.ActivateAsync(Id, A.Fake>()); + } + [Fact] public void Create_should_throw_exception_if_created() { @@ -48,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Rules sut.Create(CreateRuleCommand(command)); - Assert.Equal(AppId, sut.Snapshot.AppId); + Assert.Equal(AppId, sut.Snapshot.AppId.Id); Assert.Same(ruleTrigger, sut.Snapshot.RuleDef.Trigger); Assert.Same(ruleAction, sut.Snapshot.RuleDef.Action); @@ -239,7 +246,7 @@ namespace Squidex.Domain.Apps.Entities.Rules return CreateEvent(@event); } - protected T CreateRuleCommand(T command) where T : RuleAggregateCommand + protected T CreateRuleCommand(T command) where T : RuleCommand { command.RuleId = ruleId; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs index a3b8b5ba9..71922b093 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs @@ -8,11 +8,13 @@ using System; using System.Collections.Generic; using System.Linq; +using FakeItEasy; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.States; using Xunit; namespace Squidex.Domain.Apps.Entities.Schemas @@ -35,6 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var fieldRegistry = new FieldRegistry(new TypeNameRegistry()); sut = new SchemaDomainObject(fieldRegistry); + sut.ActivateAsync(Id, A.Fake>()); } [Fact] @@ -55,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties })); - Assert.Equal(AppId, sut.Snapshot.AppId); + Assert.Equal(AppId, sut.Snapshot.AppId.Id); Assert.Equal(SchemaName, sut.Snapshot.Name); Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); @@ -81,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var @event = (SchemaCreated)sut.GetUncomittedEvents().Single().Payload; - Assert.Equal(AppId, sut.Snapshot.AppId); + Assert.Equal(AppId, sut.Snapshot.AppId.Id); Assert.Equal(SchemaName, sut.Snapshot.Name); Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 73a745fde..0822c2f2c 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -134,16 +134,12 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers command.Actor = User; } - var appCommand = command as AppCommand; - - if (appCommand != null && appCommand.AppId == null) + if (command is IAppCommand appCommand && appCommand.AppId == null) { appCommand.AppId = AppNamedId; } - var schemaCommand = command as SchemaCommand; - - if (schemaCommand != null && schemaCommand.SchemaId == null) + if (command is ISchemaCommand schemaCommand && schemaCommand.SchemaId == null) { schemaCommand.SchemaId = SchemaNamedId; } diff --git a/tools/Migrate_01/Migration02_AddPatterns.cs b/tools/Migrate_01/Migration02_AddPatterns.cs index 279ce58cd..72fc5929a 100644 --- a/tools/Migrate_01/Migration02_AddPatterns.cs +++ b/tools/Migrate_01/Migration02_AddPatterns.cs @@ -11,7 +11,6 @@ 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; @@ -50,7 +49,7 @@ namespace Migrate_01 new AddPattern { Actor = app.Snapshot.CreatedBy, - AppId = new NamedId(app.Snapshot.Id, app.Snapshot.Name), + AppId = app.Snapshot.Id, Name = pattern.Name, PatternId = Guid.NewGuid(), Pattern = pattern.Pattern, From 66f74fcfc8cbcdfb6f0d5e8dc900b16359b8c3c9 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 20:35:53 +0100 Subject: [PATCH 14/26] Migrations fixed. --- .../MongoContentRepository_SnapshotStore.cs | 4 +- .../AppProvider.cs | 4 +- .../IAppProvider.cs | 2 +- .../Config/Domain/SerializationServices.cs | 4 +- src/Squidex/Config/Domain/WriteServices.cs | 3 ++ .../Contents/ContentQueryServiceTests.cs | 10 ++--- .../Rules/Guards/GuardRuleTests.cs | 2 +- .../Triggers/ContentChangedTriggerTests.cs | 4 +- .../Rules/RuleCommandMiddlewareTests.cs | 2 +- .../Migration05_RebuildForNewCommands.cs | 38 +++++++++++++++++++ tools/Migrate_01/SquidexMigrations.cs | 13 +++++++ 11 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 tools/Migrate_01/Migration05_RebuildForNewCommands.cs create mode 100644 tools/Migrate_01/SquidexMigrations.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index 7fa994d7f..901ce545b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return; } - var schema = await GetSchemaAsync(value.SchemaId.Id, value.SchemaId.Id); + var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id); var idData = value.Data?.ToIdModel(schema.SchemaDef, true); @@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents private async Task GetSchemaAsync(Guid appId, Guid schemaId) { - var schema = await appProvider.GetSchemaAsync(appId, schemaId); + var schema = await appProvider.GetSchemaAsync(appId, schemaId, true); if (schema == null) { diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index ee7e5cab4..b2e028a12 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -88,11 +88,11 @@ namespace Squidex.Domain.Apps.Entities return (await stateFactory.GetSingleAsync(schemaId)).Snapshot; } - public async Task GetSchemaAsync(Guid appId, Guid id) + public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) { var schema = await stateFactory.GetSingleAsync(id); - if (!IsFound(schema) || schema.Snapshot.IsDeleted || schema.Snapshot.AppId.Id != appId) + if (!IsFound(schema) || (schema.Snapshot.IsDeleted && !allowDeleted) || schema.Snapshot.AppId.Id != appId) { return null; } diff --git a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs index c41da76c9..e246b3cb7 100644 --- a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities Task GetAppAsync(string appName); - Task GetSchemaAsync(Guid appId, Guid id); + Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); Task GetSchemaAsync(Guid appId, string name); diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index c8f3cbb12..1b8f75a26 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -28,10 +28,10 @@ namespace Squidex.Config.Domain { private static readonly TypeNameRegistry TypeNameRegistry = new TypeNameRegistry() - .MapUnmapped(typeof(Migration01_FromCqrs).Assembly) .MapUnmapped(typeof(SquidexCoreModel).Assembly) .MapUnmapped(typeof(SquidexEvents).Assembly) - .MapUnmapped(typeof(SquidexInfrastructure).Assembly); + .MapUnmapped(typeof(SquidexInfrastructure).Assembly) + .MapUnmapped(typeof(SquidexMigrations).Assembly); private static readonly FieldRegistry FieldRegistry = new FieldRegistry(TypeNameRegistry); diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index 7c890b235..2a59c47fa 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -79,6 +79,9 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + services.AddTransientAs() + .As(); + services.AddTransientAs() .AsSelf(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 211f9345b..48f5338b0 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_schema_from_id_if_string_is_guid() { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); var result = await sut.FindSchemaAsync(app, schemaId.ToString()); @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_content_from_repository_and_transform() { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) .Returns(content); @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_throw_if_content_to_find_does_not_exist() { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) @@ -196,7 +196,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private void SetupFakeWithIdQuery(Status[] status, HashSet ids) { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) @@ -205,7 +205,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private void SetupFakeWithOdataQuery(Status[] status) { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs index 9d061491c..4934a4a33 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards public GuardRuleTests() { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) .Returns(A.Fake()); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs index e406ec61b..cee1ddb81 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers [Fact] public async Task Should_add_error_if_schemas_ids_are_not_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) .Returns(Task.FromResult(null)); var trigger = new ContentChangedTrigger @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers [Fact] public async Task Should_not_add_error_if_schemas_ids_are_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) .Returns(A.Fake()); var trigger = new ContentChangedTrigger diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs index 239ef4a13..863bd6d97 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public RuleCommandMiddlewareTests() { - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored)) + A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) .Returns(A.Fake()); sut = new RuleCommandMiddleware(Handler, appProvider); diff --git a/tools/Migrate_01/Migration05_RebuildForNewCommands.cs b/tools/Migrate_01/Migration05_RebuildForNewCommands.cs new file mode 100644 index 000000000..9548eb03d --- /dev/null +++ b/tools/Migrate_01/Migration05_RebuildForNewCommands.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01 +{ + public sealed class Migration05_RebuildForNewCommands : IMigration + { + private readonly Rebuilder rebuilder; + + public int FromVersion { get; } = 4; + + public int ToVersion { get; } = 5; + + public Migration05_RebuildForNewCommands(Rebuilder rebuilder) + { + this.rebuilder = rebuilder; + } + + public async Task UpdateAsync(IEnumerable previousMigrations) + { + if (!previousMigrations.Any(x => x is Migration01_FromCqrs)) + { + await rebuilder.RebuildConfigAsync(); + await rebuilder.RebuildContentAsync(); + await rebuilder.RebuildAssetsAsync(); + } + } + } +} diff --git a/tools/Migrate_01/SquidexMigrations.cs b/tools/Migrate_01/SquidexMigrations.cs new file mode 100644 index 000000000..092912098 --- /dev/null +++ b/tools/Migrate_01/SquidexMigrations.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Migrate_01 +{ + public static class SquidexMigrations + { + } +} From 3e980ddd6fe5a3caeefe9578be223588ae3ddc4a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 21:39:55 +0100 Subject: [PATCH 15/26] Scheduler improved. --- .../Contents/MongoContentEntity.cs | 16 +++++---- .../Contents/MongoContentRepository.cs | 28 +++++++++------ .../MongoContentRepository_SnapshotStore.cs | 6 ++-- .../Contents/Visitors/FindExtensions.cs | 2 +- .../Contents/Commands/ChangeContentStatus.cs | 3 ++ .../Contents/Commands/PublishContentAt.cs | 16 --------- .../Contents/ContentCommandMiddleware.cs | 17 +++------ .../Contents/ContentDomainObject.cs | 18 +++++----- .../Contents/ContentEntity.cs | 10 +++--- ...ontentPublisher.cs => ContentScheduler.cs} | 9 +++-- .../Contents/Guards/GuardContent.cs | 13 ++----- .../Contents/IContentEntity.cs | 6 ++-- .../Repositories/IContentRepository.cs | 2 +- .../Contents/State/ContentState.cs | 19 ++++++---- ...Scheduled.cs => ContentStatusScheduled.cs} | 9 +++-- .../Controllers/Content/ContentsController.cs | 35 ++++++++++++++----- 16 files changed, 111 insertions(+), 98 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs rename src/Squidex.Domain.Apps.Entities/Contents/{ContentPublisher.cs => ContentScheduler.cs} (85%) rename src/Squidex.Domain.Apps.Events/Contents/{ContentPublishScheduled.cs => ContentStatusScheduled.cs} (67%) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 1cb2f8191..b98ff890a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -35,12 +35,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonRequired] [BsonElement("ai")] [BsonRepresentation(BsonType.String)] - public Guid IdxAppId { get; set; } + public Guid AppIdId { get; set; } [BsonRequired] [BsonElement("si")] [BsonRepresentation(BsonType.String)] - public Guid IdxSchemaId { get; set; } + public Guid SchemaIdId { get; set; } [BsonRequired] [BsonElement("rf")] @@ -71,12 +71,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public NamedId SchemaId { get; set; } [BsonIgnoreIfNull] - [BsonElement("pa")] - public Instant? PublishAt { get; set; } + [BsonElement("st")] + public Status? ScheduledTo { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("sa")] + public Instant? ScheduledAt { get; set; } [BsonIgnoreIfNull] - [BsonElement("pb")] - public RefToken PublishAtBy { get; set; } + [BsonElement("sb")] + public RefToken ScheduledBy { get; set; } [BsonRequired] [BsonElement("ct")] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index ac05250e7..1d4cc508f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -52,6 +52,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { await collection.Indexes.TryDropOneAsync("si_1_st_1_dl_1_dt_text"); + await archiveCollection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.ScheduledTo)); + await archiveCollection.Indexes.CreateOneAsync( Index .Ascending(x => x.Id) @@ -60,13 +64,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collection.Indexes.CreateOneAsync( Index .Text(x => x.DataText) - .Ascending(x => x.IdxSchemaId) + .Ascending(x => x.SchemaIdId) .Ascending(x => x.Status) .Ascending(x => x.IsDeleted)); await collection.Indexes.CreateOneAsync( Index - .Ascending(x => x.IdxSchemaId) + .Ascending(x => x.SchemaIdId) .Ascending(x => x.Id) .Ascending(x => x.IsDeleted) .Ascending(x => x.Status)); @@ -122,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { - var find = Collection.Find(x => x.IdxSchemaId == schema.Id && ids.Contains(x.Id) && x.IsDeleted == false && status.Contains(x.Status)); + var find = Collection.Find(x => x.SchemaIdId == schema.Id && ids.Contains(x.Id) && x.IsDeleted == false && status.Contains(x.Status)); var contentItems = find.ToListAsync(); var contentCount = find.CountAsync(); @@ -140,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList ids) { var contentEntities = - await Collection.Find(x => x.IdxSchemaId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id) + await Collection.Find(x => x.SchemaIdId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id) .ToListAsync(); return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList(); @@ -160,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) { var contentEntity = - await Collection.Find(x => x.IdxSchemaId == schema.Id && x.Id == id && x.IsDeleted == false) + await Collection.Find(x => x.SchemaIdId == schema.Id && x.Id == id && x.IsDeleted == false) .FirstOrDefaultAsync(); contentEntity?.ParseData(schema.SchemaDef); @@ -168,16 +172,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return contentEntity; } + public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) + { + return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted == false) + .ForEachAsync(c => + { + callback(c); + }); + } + public override async Task ClearAsync() { await Database.DropCollectionAsync("States_Contents_Archive"); await base.ClearAsync(); } - - public Task QueryContentToPublishAsync(Instant now, Func callback) - { - throw new NotSupportedException(); - } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index 901ce545b..54e236e4a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents if (contentEntity != null) { - var schema = await GetSchemaAsync(contentEntity.IdxAppId, contentEntity.IdxSchemaId); + var schema = await GetSchemaAsync(contentEntity.AppIdId, contentEntity.SchemaIdId); contentEntity?.ParseData(schema.SchemaDef); @@ -53,8 +53,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents var document = SimpleMapper.Map(value, new MongoContentEntity { - IdxAppId = value.AppId.Id, - IdxSchemaId = value.SchemaId.Id, + AppIdId = value.AppId.Id, + SchemaIdId = value.SchemaId.Id, IsDeleted = value.IsDeleted, DocumentId = key.ToString(), DataText = idData?.ToFullText(), diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 8f9118e1c..cdfaff9b8 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { var filters = new List> { - Filter.Eq(x => x.IdxSchemaId, schemaId), + Filter.Eq(x => x.SchemaIdId, schemaId), Filter.In(x => x.Status, status), Filter.Eq(x => x.IsDeleted, false) }; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs index 9e8de0bd2..5ca260cc4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================= +using NodaTime; using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Entities.Contents.Commands @@ -12,5 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public sealed class ChangeContentStatus : ContentCommand { public Status Status { get; set; } + + public Instant? DueDate { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs deleted file mode 100644 index 3b20c1aeb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PublishContentAt.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Entities.Contents.Commands -{ - public sealed class PublishContentAt : ContentDataCommand - { - public DateTimeOffset PublishAt { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs index dec465b99..73f4619f1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -109,9 +109,12 @@ namespace Squidex.Domain.Apps.Entities.Contents { GuardContent.CanChangeContentStatus(content.Snapshot.Status, command); - var operationContext = await CreateContext(command, content, () => "Failed to patch content."); + if (!command.DueDate.HasValue) + { + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); + } content.ChangeStatus(command); }); @@ -131,16 +134,6 @@ namespace Squidex.Domain.Apps.Entities.Contents }); } - protected Task On(PublishContentAt command, CommandContext context) - { - return handler.UpdateAsync(context, content => - { - GuardContent.CanPublishAt(command); - - content.PublishAt(command); - }); - } - public async Task HandleAsync(CommandContext context, Func next) { await this.DispatchActionAsync(context.Command, context); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index dd7e4fc05..e27a947a4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -46,16 +46,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { VerifyCreatedAndNotDeleted(); - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); - - return this; - } - - public ContentDomainObject PublishAt(PublishContentAt command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new ContentPublishScheduled())); + if (command.DueDate.HasValue) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled())); + } + else + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); + } return this; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 84269d0e5..27e1c1895 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -28,9 +28,13 @@ namespace Squidex.Domain.Apps.Entities.Contents public Instant LastModified { get; set; } - public Instant? PublishAt { get; set; } + public Status Status { get; set; } + + public Status? ScheduledTo { get; set; } + + public Instant? ScheduledAt { get; set; } - public RefToken PublishAtBy { get; set; } + public RefToken ScheduledBy { get; set; } public RefToken CreatedBy { get; set; } @@ -38,8 +42,6 @@ namespace Squidex.Domain.Apps.Entities.Contents public NamedContentData Data { get; set; } - public Status Status { get; set; } - public static ContentEntity Create(CreateContent command, EntityCreatedResult result) { var now = SystemClock.Instance.GetCurrentInstant(); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs similarity index 85% rename from src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs rename to src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs index 026a53296..23d3c05a9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentPublisher.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using NodaTime; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Infrastructure; @@ -16,14 +15,14 @@ using Squidex.Infrastructure.Timers; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentPublisher : IRunnable + public sealed class ContentScheduler : IRunnable { private readonly CompletionTimer timer; private readonly IContentRepository contentRepository; private readonly ICommandBus commandBus; private readonly IClock clock; - public ContentPublisher( + public ContentScheduler( IContentRepository contentRepository, ICommandBus commandBus, IClock clock) @@ -47,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var now = clock.GetCurrentInstant(); - return contentRepository.QueryContentToPublishAsync(now, content => + return contentRepository.QueryScheduledWithoutDataAsync(now, content => { - var command = new ChangeContentStatus { ContentId = content.Id, Status = Status.Published, Actor = content.PublishAtBy }; + var command = new ChangeContentStatus { ContentId = content.Id, Status = content.ScheduledTo.Value, Actor = content.ScheduledBy }; return commandBus.PublishAsync(command); }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 092c144df..00cd6526a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; @@ -63,18 +64,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); } - }); - } - - public static void CanPublishAt(PublishContentAt command) - { - Guard.NotNull(command, nameof(command)); - Validate.It(() => "Cannot schedule content tol publish.", error => - { - if (command.PublishAt < DateTime.UtcNow) + if (command.DueDate.HasValue && command.DueDate.Value < SystemClock.Instance.GetCurrentInstant()) { - error(new ValidationError("Date must be in the future.", nameof(command.PublishAt))); + error(new ValidationError("DueDate must be in the future.", nameof(command.DueDate))); } }); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs index d17ccc2e7..11a33154c 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -25,9 +25,11 @@ namespace Squidex.Domain.Apps.Entities.Contents Status Status { get; } - Instant? PublishAt { get; } + Status? ScheduledTo { get; } - RefToken PublishAtBy { get; } + Instant? ScheduledAt { get; } + + RefToken ScheduledBy { get; } NamedContentData Data { get; } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 3f925d049..b9aba61be 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -29,6 +29,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version); - Task QueryContentToPublishAsync(Instant now, Func callback); + Task QueryScheduledWithoutDataAsync(Instant now, Func callback); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 75bb8f6d2..489a03832 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -33,10 +33,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.State public Status Status { get; set; } [JsonProperty] - public RefToken PublishAtBy { get; set; } + public Status? ScheduledTo { get; set; } [JsonProperty] - public Instant? PublishAt { get; set; } + public Instant? ScheduledAt { get; set; } + + [JsonProperty] + public RefToken ScheduledBy { get; set; } [JsonProperty] public bool IsDeleted { get; set; } @@ -55,18 +58,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.State Data = @event.Data; } - protected void On(ContentPublishScheduled @event) + protected void On(ContentStatusScheduled @event) { - PublishAt = @event.PublishAt; - PublishAtBy = @event.Actor; + ScheduledAt = @event.DueTime; + ScheduledBy = @event.Actor; + ScheduledTo = @event.Status; } protected void On(ContentStatusChanged @event) { Status = @event.Status; - PublishAt = null; - PublishAtBy = null; + ScheduledAt = null; + ScheduledBy = null; + ScheduledTo = null; } protected void On(ContentDeleted @event) diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs similarity index 67% rename from src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs rename to src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs index 50ef1581f..e0d0a5bac 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentPublishScheduled.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs @@ -6,13 +6,16 @@ // ========================================================================== using NodaTime; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Contents { - [EventType(nameof(ContentPublishScheduled))] - public sealed class ContentPublishScheduled : ContentEvent + [EventType(nameof(ContentStatusScheduled))] + public sealed class ContentStatusScheduled : ContentEvent { - public Instant PublishAt { get; set; } + public Status Status { get; set; } + + public Instant DueTime { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index 9fb56d378..812bdc9cd 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using NodaTime; +using NodaTime.Text; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Core.Contents; @@ -213,11 +215,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/publish/")] [ApiCosts(1)] - public async Task PublishContent(string name, Guid id) + public async Task PublishContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Published, ContentId = id }; + var command = CreateCommand(id, Status.Published, dueDate); await CommandBus.PublishAsync(command); @@ -228,11 +230,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/unpublish/")] [ApiCosts(1)] - public async Task UnpublishContent(string name, Guid id) + public async Task UnpublishContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id }; + var command = CreateCommand(id, Status.Draft, dueDate); await CommandBus.PublishAsync(command); @@ -243,11 +245,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/archive/")] [ApiCosts(1)] - public async Task ArchiveContent(string name, Guid id) + public async Task ArchiveContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Archived, ContentId = id }; + var command = CreateCommand(id, Status.Archived, dueDate); await CommandBus.PublishAsync(command); @@ -258,11 +260,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/restore/")] [ApiCosts(1)] - public async Task RestoreContent(string name, Guid id) + public async Task RestoreContent(string name, Guid id, string dueDate = null) { await contentQuery.FindSchemaAsync(App, name); - var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id }; + var command = CreateCommand(id, Status.Draft, dueDate); await CommandBus.PublishAsync(command); @@ -283,5 +285,22 @@ namespace Squidex.Areas.Api.Controllers.Contents return NoContent(); } + + private static ChangeContentStatus CreateCommand(Guid id, Status status, string dueDate) + { + Instant? dt = null; + + if (string.IsNullOrWhiteSpace(dueDate)) + { + var parseResult = InstantPattern.General.Parse(dueDate); + + if (!parseResult.Success) + { + dt = parseResult.Value; + } + } + + return new ChangeContentStatus { Status = status, ContentId = id, DueDate = dt }; + } } } From 7723799ab37225c11a3990a279c1e19518239fc9 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 21:41:18 +0100 Subject: [PATCH 16/26] Property renamed. --- .../Assets/MongoAssetEntity.cs | 2 +- .../Assets/MongoAssetRepository_SnapshotStore.cs | 2 +- .../Assets/Visitors/FindExtensions.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 2beb7522a..2f2729858 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { [BsonRequired] [BsonElement] - public Guid IdxAppId { get; set; } + public Guid AppIdId { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index 2573debe2..a4056d0a7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets var entity = SimpleMapper.Map(value, new MongoAssetEntity()); entity.Version = newVersion; - entity.IdxAppId = value.AppId.Id; + entity.AppIdId = value.AppId.Id; await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index f8ba52bf6..af3a75764 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors { var filters = new List> { - Filter.Eq(x => x.IdxAppId, appId), + Filter.Eq(x => x.AppIdId, appId), Filter.Eq(x => x.IsDeleted, false) }; From a13943bf182e1801696240c6152b71223db5af23 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 21:52:04 +0100 Subject: [PATCH 17/26] * DueDate => DueTime (renamed) * Tests added * Parsing due time from query string. --- .../Contents/MongoContentEntity.cs | 6 ++--- .../Contents/Commands/ChangeContentStatus.cs | 2 +- .../Contents/ContentCommandMiddleware.cs | 2 +- .../Contents/ContentDomainObject.cs | 4 ++-- .../Contents/Guards/GuardContent.cs | 4 ++-- .../Controllers/Content/ContentsController.cs | 24 +++++++++---------- .../Contents/ContentCommandMiddlewareTests.cs | 16 +++++++++++++ .../Contents/ContentDomainObjectTests.cs | 20 ++++++++++++++++ .../Contents/Guard/GuardContentTests.cs | 11 +++++++++ 9 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index b98ff890a..f35cb2286 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -71,15 +71,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public NamedId SchemaId { get; set; } [BsonIgnoreIfNull] - [BsonElement("st")] + [BsonElement("sdt")] public Status? ScheduledTo { get; set; } [BsonIgnoreIfNull] - [BsonElement("sa")] + [BsonElement("sda")] public Instant? ScheduledAt { get; set; } [BsonIgnoreIfNull] - [BsonElement("sb")] + [BsonElement("sdb")] public RefToken ScheduledBy { get; set; } [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs index 5ca260cc4..e855a9aff 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands { public Status Status { get; set; } - public Instant? DueDate { get; set; } + public Instant? DueTime { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs index 73f4619f1..7674f1512 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { GuardContent.CanChangeContentStatus(content.Snapshot.Status, command); - if (!command.DueDate.HasValue) + if (!command.DueTime.HasValue) { var operationContext = await CreateContext(command, content, () => "Failed to patch content."); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index e27a947a4..3cda651fb 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -46,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { VerifyCreatedAndNotDeleted(); - if (command.DueDate.HasValue) + if (command.DueTime.HasValue) { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled())); + RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); } else { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 00cd6526a..dfd5d8b68 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -65,9 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); } - if (command.DueDate.HasValue && command.DueDate.Value < SystemClock.Instance.GetCurrentInstant()) + if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant()) { - error(new ValidationError("DueDate must be in the future.", nameof(command.DueDate))); + error(new ValidationError("DueTime must be in the future.", nameof(command.DueTime))); } }); } diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index 812bdc9cd..fc81dbc70 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -215,11 +215,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/publish/")] [ApiCosts(1)] - public async Task PublishContent(string name, Guid id, string dueDate = null) + public async Task PublishContent(string name, Guid id, string dueTime = null) { await contentQuery.FindSchemaAsync(App, name); - var command = CreateCommand(id, Status.Published, dueDate); + var command = CreateCommand(id, Status.Published, dueTime); await CommandBus.PublishAsync(command); @@ -230,11 +230,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/unpublish/")] [ApiCosts(1)] - public async Task UnpublishContent(string name, Guid id, string dueDate = null) + public async Task UnpublishContent(string name, Guid id, string dueTime = null) { await contentQuery.FindSchemaAsync(App, name); - var command = CreateCommand(id, Status.Draft, dueDate); + var command = CreateCommand(id, Status.Draft, dueTime); await CommandBus.PublishAsync(command); @@ -245,11 +245,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/archive/")] [ApiCosts(1)] - public async Task ArchiveContent(string name, Guid id, string dueDate = null) + public async Task ArchiveContent(string name, Guid id, string dueTime = null) { await contentQuery.FindSchemaAsync(App, name); - var command = CreateCommand(id, Status.Archived, dueDate); + var command = CreateCommand(id, Status.Archived, dueTime); await CommandBus.PublishAsync(command); @@ -260,11 +260,11 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/restore/")] [ApiCosts(1)] - public async Task RestoreContent(string name, Guid id, string dueDate = null) + public async Task RestoreContent(string name, Guid id, string dueTime = null) { await contentQuery.FindSchemaAsync(App, name); - var command = CreateCommand(id, Status.Draft, dueDate); + var command = CreateCommand(id, Status.Draft, dueTime); await CommandBus.PublishAsync(command); @@ -286,13 +286,13 @@ namespace Squidex.Areas.Api.Controllers.Contents return NoContent(); } - private static ChangeContentStatus CreateCommand(Guid id, Status status, string dueDate) + private static ChangeContentStatus CreateCommand(Guid id, Status status, string dueTime) { Instant? dt = null; - if (string.IsNullOrWhiteSpace(dueDate)) + if (string.IsNullOrWhiteSpace(dueTime)) { - var parseResult = InstantPattern.General.Parse(dueDate); + var parseResult = InstantPattern.General.Parse(dueTime); if (!parseResult.Success) { @@ -300,7 +300,7 @@ namespace Squidex.Areas.Api.Controllers.Contents } } - return new ChangeContentStatus { Status = status, ContentId = id, DueDate = dt }; + return new ChangeContentStatus { Status = status, ContentId = id, DueTime = dt }; } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs index 0d9f45594..113b574da 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -9,6 +9,7 @@ using System; using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; +using NodaTime; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; @@ -225,6 +226,21 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); } + [Fact] + public async Task ChangeStatus_should_not_invoke_scripts_when_scheduled() + { + CreateContent(); + + var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published, DueTime = Instant.MaxValue }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); + } + [Fact] public async Task Delete_should_update_domain_object() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index b0a061422..59e7e0213 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -8,6 +8,7 @@ using System; using FakeItEasy; using FluentAssertions; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -207,6 +208,25 @@ namespace Squidex.Domain.Apps.Entities.Contents ); } + [Fact] + public void ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time() + { + CreateContent(); + + var dueTime = Instant.MaxValue; + + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published, DueTime = dueTime })); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + Assert.Equal(Status.Published, sut.Snapshot.ScheduledTo); + Assert.Equal(dueTime, sut.Snapshot.ScheduledAt); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) + ); + } + [Fact] public void Delete_should_throw_exception_if_not_created() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs index 60dcbbeef..587a62955 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Guards; @@ -15,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard { public class GuardContentTests { + private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); + [Fact] public void CanCreate_should_throw_exception_if_data_is_null() { @@ -79,6 +82,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); } + [Fact] + public void CanChangeContentStatus_should_throw_exception_if_due_date_in_past() + { + var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast }; + + Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Draft, command)); + } + [Fact] public void CanChangeContentStatus_not_should_throw_exception_if_status_flow_valid() { From 91c5053ff8819774dda6a5b96e3b76e446416f80 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 23:09:41 +0100 Subject: [PATCH 18/26] UI fixed and simplified. --- .../Controllers/Content/Models/ContentDto.cs | 15 +++ .../pages/content/content-page.component.ts | 35 +++--- .../pages/contents/contents-page.component.ts | 70 +++--------- .../app/features/content/pages/messages.ts | 11 +- .../shared/services/assets.service.spec.ts | 2 +- .../shared/services/contents.service.spec.ts | 94 ++++++---------- .../app/shared/services/contents.service.ts | 104 ++++++++---------- 7 files changed, 134 insertions(+), 197 deletions(-) diff --git a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs index eee19d42f..f2c16cedd 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs @@ -40,6 +40,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models [Required] public object Data { get; set; } + /// + /// The scheduled status. + /// + public Status? ScheduledTo { get; } + + /// + /// The scheduled date. + /// + public Instant? ScheduledAt { get; } + + /// + /// The user that has scheduled the content. + /// + public RefToken ScheduledBy { get; } + /// /// The date and time when the content item has been created. /// diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 0b6467631..4dd2a16a4 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -12,9 +12,8 @@ import { Observable, Subscription } from 'rxjs'; import { ContentCreated, - ContentPublished, ContentRemoved, - ContentUnpublished, + ContentStatusChanged, ContentUpdated, ContentVersionSelected } from './../messages'; @@ -39,8 +38,7 @@ import { ] }) export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { - private contentPublishedSubscription: Subscription; - private contentUnpublishedSubscription: Subscription; + private contentStatusChangedSubscription: Subscription; private contentDeletedSubscription: Subscription; private contentVersionSelectedSubscription: Subscription; @@ -63,8 +61,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, public ngOnDestroy() { this.contentVersionSelectedSubscription.unsubscribe(); - this.contentUnpublishedSubscription.unsubscribe(); - this.contentPublishedSubscription.unsubscribe(); + this.contentStatusChangedSubscription.unsubscribe(); this.contentDeletedSubscription.unsubscribe(); } @@ -75,27 +72,25 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, this.loadVersion(message.version); }); - this.contentPublishedSubscription = - this.ctx.bus.of(ContentPublished) - .subscribe(message => { - if (this.content && message.content.id === this.content.id) { - this.content = this.content.publish(message.content.lastModifiedBy, message.content.version, message.content.lastModified); - } - }); - - this.contentUnpublishedSubscription = - this.ctx.bus.of(ContentUnpublished) + this.contentDeletedSubscription = + this.ctx.bus.of(ContentRemoved) .subscribe(message => { if (this.content && message.content.id === this.content.id) { - this.content = this.content.unpublish(message.content.lastModifiedBy, message.content.version, message.content.lastModified); + this.router.navigate(['../'], { relativeTo: this.ctx.route }); } }); - this.contentDeletedSubscription = - this.ctx.bus.of(ContentRemoved) + this.contentStatusChangedSubscription = + this.ctx.bus.of(ContentStatusChanged) .subscribe(message => { if (this.content && message.content.id === this.content.id) { - this.router.navigate(['../'], { relativeTo: this.ctx.route }); + this.content = + this.content.changeStatus( + message.content.scheduledTo || message.content.status, + message.content.lastModifiedBy, + message.content.version, + message.content.lastModified, + message.content.scheduledAt); } }); diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index a00c846b8..ec262312c 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -11,9 +11,8 @@ import { Observable, Subscription } from 'rxjs'; import { ContentCreated, - ContentPublished, ContentRemoved, - ContentUnpublished, + ContentStatusChanged, ContentUpdated } from './../messages'; @@ -27,7 +26,8 @@ import { ImmutableArray, ModalView, Pager, - SchemaDetailsDto + SchemaDetailsDto, + DateTime } from 'shared'; @Component({ @@ -118,7 +118,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public publishContent(content: ContentDto) { - this.publishContentItem(content).subscribe(); + this.changeContentItem(content, 'publish', 'Published').subscribe(); } public publishSelected() { @@ -126,29 +126,15 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.contentItems.values .filter(c => this.selectedItems[c.id]) .filter(c => c.status !== 'Published') - .map(c => this.publishContentItem(c))) + .map(c => this.changeContentItem(c, 'publish', 'Published'))) .finally(() => { this.updateSelectionSummary(); }) .subscribe(); } - private publishContentItem(content: ContentDto): Observable { - return this.contentsService.publishContent(this.ctx.appName, this.schema.name, content.id, content.version) - .catch(error => { - this.ctx.notifyError(error); - - return Observable.throw(error); - }) - .do(dto => { - this.contentItems = this.contentItems.replaceBy('id', content.publish(this.ctx.userToken, dto.version)); - - this.emitContentPublished(content); - }); - } - public unpublishContent(content: ContentDto) { - this.unpublishContentItem(content).subscribe(); + this.changeContentItem(content, 'unpublish', 'Draft').subscribe(); } public unpublishSelected() { @@ -156,31 +142,31 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.contentItems.values .filter(c => this.selectedItems[c.id]) .filter(c => c.status !== 'Unpublished') - .map(c => this.unpublishContentItem(c))) + .map(c => this.changeContentItem(c, 'unpublish', 'Draft'))) .finally(() => { this.updateSelectionSummary(); }) .subscribe(); } - private unpublishContentItem(content: ContentDto): Observable { - return this.contentsService.unpublishContent(this.ctx.appName, this.schema.name, content.id, content.version) + private changeContentItem(content: ContentDto, action: string, status: string): Observable { + return this.contentsService.changeContentStatus(this.ctx.appName, this.schema.name, content.id, action, content.version) .catch(error => { this.ctx.notifyError(error); return Observable.throw(error); }) .do(dto => { - this.contentItems = this.contentItems.replaceBy('id', content.unpublish(this.ctx.userToken, dto.version)); + this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, this.ctx.userToken, dto.version)); - this.emitContentUnpublished(content); + this.emitContentStatusChanged(content); }); } public archiveSelected() { Observable.forkJoin( this.contentItems.values.filter(c => this.selectedItems[c.id]) - .map(c => this.archiveContentItem(c))) + .map(c => this.changeContentItem(c, 'archive', 'Archived'))) .finally(() => { this.load(); }) @@ -188,26 +174,17 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public archiveContent(content: ContentDto) { - this.archiveContentItem(content) + this.changeContentItem(content, 'archive', 'Archived') .finally(() => { this.load(); }) .subscribe(); } - public archiveContentItem(content: ContentDto): Observable { - return this.contentsService.archiveContent(this.ctx.appName, this.schema.name, content.id, content.version) - .catch(error => { - this.ctx.notifyError(error); - - return Observable.throw(error); - }); - } - public restoreSelected() { Observable.forkJoin( this.contentItems.values.filter(c => this.selectedItems[c.id]) - .map(c => this.restoreContentItem(c))) + .map(c => this.changeContentItem(c, 'restore', 'Draft'))) .finally(() => { this.load(); }) @@ -215,22 +192,13 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public restoreContent(content: ContentDto) { - this.restoreContentItem(content) + this.changeContentItem(content, 'restore', 'Draft') .finally(() => { this.load(); }) .subscribe(); } - public restoreContentItem(content: ContentDto): Observable { - return this.contentsService.restoreContent(this.ctx.appName, this.schema.name, content.id, content.version) - .catch(error => { - this.ctx.notifyError(error); - - return Observable.throw(error); - }); - } - public deleteSelected(content: ContentDto) { Observable.forkJoin( this.contentItems.values.filter(c => this.selectedItems[c.id]) @@ -359,12 +327,8 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.languageSelected = language; } - private emitContentPublished(content: ContentDto) { - this.ctx.bus.emit(new ContentPublished(content)); - } - - private emitContentUnpublished(content: ContentDto) { - this.ctx.bus.emit(new ContentUnpublished(content)); + private emitContentStatusChanged(content: ContentDto) { + this.ctx.bus.emit(new ContentStatusChanged(content)); } private emitContentRemoved(content: ContentDto) { diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 896f677e2..ca5a19fa4 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ContentDto } from 'shared'; +import { ContentDto, DateTime } from 'shared'; export class ContentCreated { constructor( @@ -35,14 +35,7 @@ export class ContentVersionSelected { } } -export class ContentPublished { - constructor( - public readonly content: ContentDto - ) { - } -} - -export class ContentUnpublished { +export class ContentStatusChanged { constructor( public readonly content: ContentDto ) { diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 1fb922072..09cb9af92 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -89,7 +89,7 @@ describe('AssetsService', () => { }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?$top=17&$skip=13'); - + expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 396fd3a2c..af7b1dc45 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -23,11 +23,12 @@ describe('ContentDto', () => { const creator = 'not-me'; const modified = DateTime.now(); const modifier = 'me'; + const dueTime = DateTime.now().addDays(1); const version = new Version('1'); const newVersion = new Version('2'); it('should update data property and user info when updating', () => { - const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); + const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, null, null, null, { data: 1 }, version); const content_2 = content_1.update({ data: 2 }, modifier, newVersion, modified); expect(content_2.data).toEqual({ data: 2 }); @@ -36,9 +37,9 @@ describe('ContentDto', () => { expect(content_2.version).toEqual(newVersion); }); - it('should update status property and user info when publishing', () => { - const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.publish(modifier, newVersion, modified); + it('should update status property and user info when changing status', () => { + const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version); + const content_2 = content_1.changeStatus('Published', modifier, newVersion, modified); expect(content_2.status).toEqual('Published'); expect(content_2.lastModified).toEqual(modified); @@ -46,40 +47,23 @@ describe('ContentDto', () => { expect(content_2.version).toEqual(newVersion); }); - it('should update status property and user info when unpublishing', () => { - const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.unpublish(modifier, newVersion, modified); - - expect(content_2.status).toEqual('Draft'); - expect(content_2.lastModified).toEqual(modified); - expect(content_2.lastModifiedBy).toEqual(modifier); - expect(content_2.version).toEqual(newVersion); - }); - - it('should update status property and user info when archiving', () => { - const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.archive(modifier, newVersion, modified); - - expect(content_2.status).toEqual('Archived'); - expect(content_2.lastModified).toEqual(modified); - expect(content_2.lastModifiedBy).toEqual(modifier); - expect(content_2.version).toEqual(newVersion); - }); - - it('should update status property and user info when restoring', () => { - const content_1 = new ContentDto('1', 'Archived', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.restore(modifier, newVersion, modified); + it('should update schedules property and user info when changing status with due time', () => { + const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version); + const content_2 = content_1.changeStatus('Published', modifier, newVersion, modified, dueTime); expect(content_2.status).toEqual('Draft'); expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModifiedBy).toEqual(modifier); + expect(content_2.scheduledAt).toEqual(dueTime); + expect(content_2.scheduledBy).toEqual(modifier); + expect(content_2.scheduledTo).toEqual('Published'); expect(content_2.version).toEqual(newVersion); }); it('should update data property when setting data', () => { const newData = {}; - const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); + const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, null, null, null, { data: 1 }, version); const content_2 = content_1.setData(newData); expect(content_2.data).toBe(newData); @@ -130,6 +114,9 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + scheduledTo: 'Draft', + scheduledBy: 'Scheduler1', + scheduledAt: '2018-12-12T10:10', version: 11, data: {} }, @@ -151,11 +138,17 @@ describe('ContentsService', () => { new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), + 'Draft', + 'Scheduler1', + DateTime.parseISO_UTC('2018-12-12T10:10'), {}, new Version('11')), new ContentDto('id2', 'Published', 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), + null, + null, + null, {}, new Version('22')) ])); @@ -221,6 +214,9 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + scheduledTo: 'Draft', + scheduledBy: 'Scheduler1', + scheduledAt: '2018-12-12T10:10', data: {} }, { headers: { @@ -232,6 +228,9 @@ describe('ContentsService', () => { new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), + 'Draft', + 'Scheduler1', + DateTime.parseISO_UTC('2018-12-12T10:10'), {}, new Version('2'))); })); @@ -270,6 +269,9 @@ describe('ContentsService', () => { new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), + null, + null, + null, {}, new Version('2'))); })); @@ -310,10 +312,10 @@ describe('ContentsService', () => { req.flush({}); })); - it('should make put request to publish content', + it('should make put request to change content status', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - contentsService.publishContent('my-app', 'my-schema', 'content1', version).subscribe(); + contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', version).subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/publish'); @@ -323,38 +325,14 @@ describe('ContentsService', () => { req.flush({}); })); - it('should make put request to unpublish content', - inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - - contentsService.unpublishContent('my-app', 'my-schema', 'content1', version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/unpublish'); - - expect(req.request.method).toEqual('PUT'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - - req.flush({}); - })); - - it('should make put request to archive content', + it('should make put request with due time when status change is scheduled', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - contentsService.archiveContent('my-app', 'my-schema', 'content1', version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/archive'); - - expect(req.request.method).toEqual('PUT'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - - req.flush({}); - })); - - it('should make put request to restore content', - inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { + const dueTime = DateTime.parseISO_UTC('2016-12-12T10:10'); - contentsService.restoreContent('my-app', 'my-schema', 'content1', version).subscribe(); + contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', version, dueTime).subscribe(); - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/restore'); + const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/publish?dueTime=2016-12-12T10:10:00.000Z'); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toEqual(version.value); diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 553912441..3b87d6874 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -36,6 +36,9 @@ export class ContentDto { public readonly lastModifiedBy: string, public readonly created: DateTime, public readonly lastModified: DateTime, + public readonly scheduledTo: string | null, + public readonly scheduledBy: string | null, + public readonly scheduledAt: DateTime | null, public readonly data: any, public readonly version: Version ) { @@ -49,34 +52,37 @@ export class ContentDto { this.lastModifiedBy, this.created, this.lastModified, + this.scheduledTo, + this.scheduledBy, + this.scheduledAt, data, this.version); } - public publish(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Published', user, version, now); - } - - public unpublish(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Draft', user, version, now); - } - - public archive(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Archived', user, version, now); - } - - public restore(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Draft', user, version, now); - } - - private changeStatus(status: string, user: string, version: Version, now?: DateTime): ContentDto { - return new ContentDto( - this.id, - status, - this.createdBy, user, - this.created, now || DateTime.now(), - this.data, - version); + public changeStatus(status: string, user: string, version: Version, now?: DateTime, dueTime: DateTime | null = null): ContentDto { + if (dueTime) { + return new ContentDto( + this.id, + this.status, + this.createdBy, user, + this.created, now || DateTime.now(), + status, + user, + dueTime, + this.data, + version); + } else { + return new ContentDto( + this.id, + status, + this.createdBy, user, + this.created, now || DateTime.now(), + null, + null, + null, + this.data, + version); + } } public update(data: any, user: string, version: Version, now?: DateTime): ContentDto { @@ -85,6 +91,9 @@ export class ContentDto { this.status, this.createdBy, user, this.created, now || DateTime.now(), + this.scheduledTo, + this.scheduledBy, + this.scheduledAt, data, version); } @@ -146,6 +155,9 @@ export class ContentsService { item.lastModifiedBy, DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.lastModified), + item.scheduledTo || null, + item.scheduledBy || null, + item.scheduledAt ? DateTime.parseISO_UTC(item.scheduledAt) : null, item.data, new Version(item.version.toString())); })); @@ -167,6 +179,9 @@ export class ContentsService { body.lastModifiedBy, DateTime.parseISO_UTC(body.created), DateTime.parseISO_UTC(body.lastModified), + body.scheduledTo || null, + body.scheduledBy || null, + body.scheduledAt || null ? DateTime.parseISO_UTC(body.scheduledAt) : null, body.data, response.version); }) @@ -197,6 +212,9 @@ export class ContentsService { body.lastModifiedBy, DateTime.parseISO_UTC(body.created), DateTime.parseISO_UTC(body.lastModified), + null, + null, + null, body.data, response.version); }) @@ -231,43 +249,17 @@ export class ContentsService { .pretifyError('Failed to delete content. Please reload.'); } - public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); - - return HTTP.putVersioned(this.http, url, {}, version) - .do(() => { - this.analytics.trackEvent('Content', 'Published', appName); - }) - .pretifyError('Failed to publish content. Please reload.'); - } - - public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`); - - return HTTP.putVersioned(this.http, url, {}, version) - .do(() => { - this.analytics.trackEvent('Content', 'Unpublished', appName); - }) - .pretifyError('Failed to unpublish content. Please reload.'); - } + public changeContentStatus(appName: string, schemaName: string, id: string, action: string, version: Version, dueTime?: DateTime): Observable> { + let url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${action}`); - public archiveContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/archive`); + if (dueTime) { + url += `?dueTime=${dueTime.toISOString()}`; + } return HTTP.putVersioned(this.http, url, {}, version) .do(() => { this.analytics.trackEvent('Content', 'Archived', appName); }) - .pretifyError('Failed to archive content. Please reload.'); - } - - public restoreContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/restore`); - - return HTTP.putVersioned(this.http, url, {}, version) - .do(() => { - this.analytics.trackEvent('Content', 'Restored', appName); - }) - .pretifyError('Failed to restore content. Please reload.'); + .pretifyError(`Failed to ${action} content. Please reload.`); } } \ No newline at end of file From 22ed694eb1f72534d6292936084f8ce77c1b71a7 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 10 Feb 2018 11:39:00 +0100 Subject: [PATCH 19/26] UI updated. --- .../Controllers/Content/ContentsController.cs | 4 +- .../Controllers/Content/Models/ContentDto.cs | 6 +- .../pages/content/content-page.component.ts | 4 +- .../contents/contents-page.component.html | 52 +- .../pages/contents/contents-page.component.ts | 142 ++- .../app/features/content/pages/messages.ts | 2 +- .../shared/content-item.component.html | 21 +- .../shared/content-item.component.scss | 31 +- .../content/shared/content-item.component.ts | 4 +- .../events/rule-events-page.component.html | 2 +- .../pages/rules/rule-wizard.component.scss | 4 - .../schema/schema-scripts-form.component.scss | 4 - .../angular/date-time-editor.component.html | 2 +- .../angular/date-time-editor.component.ts | 5 +- .../framework/angular/date-time.pipes.spec.ts | 12 + .../app/framework/angular/date-time.pipes.ts | 10 + .../angular/dialog-renderer.component.scss | 4 - .../angular/onboarding-tooltip.component.html | 1 - .../angular/onboarding-tooltip.component.scss | 4 +- src/Squidex/app/framework/module.ts | 3 + .../shared/services/contents.service.spec.ts | 12 +- .../app/shared/services/contents.service.ts | 6 +- src/Squidex/app/theme/_bootstrap.scss | 6 + src/Squidex/app/theme/_lists.scss | 4 + src/Squidex/app/theme/_panels.scss | 21 +- src/Squidex/app/theme/_vars.scss | 1 + .../app/theme/icomoon/demo-files/demo.css | 4 +- src/Squidex/app/theme/icomoon/demo.html | 372 +++--- .../app/theme/icomoon/fonts/icomoon.eot | Bin 23028 -> 23328 bytes .../app/theme/icomoon/fonts/icomoon.svg | 2 + .../app/theme/icomoon/fonts/icomoon.ttf | Bin 22864 -> 23164 bytes .../app/theme/icomoon/fonts/icomoon.woff | Bin 22940 -> 23240 bytes src/Squidex/app/theme/icomoon/selection.json | 1098 +++++++++-------- src/Squidex/app/theme/icomoon/style.css | 94 +- 34 files changed, 1080 insertions(+), 857 deletions(-) diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index fc81dbc70..6cc59d1b7 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -290,11 +290,11 @@ namespace Squidex.Areas.Api.Controllers.Contents { Instant? dt = null; - if (string.IsNullOrWhiteSpace(dueTime)) + if (!string.IsNullOrWhiteSpace(dueTime)) { var parseResult = InstantPattern.General.Parse(dueTime); - if (!parseResult.Success) + if (parseResult.Success) { dt = parseResult.Value; } diff --git a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs index f2c16cedd..2be029606 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs @@ -43,17 +43,17 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// /// The scheduled status. /// - public Status? ScheduledTo { get; } + public Status? ScheduledTo { get; set; } /// /// The scheduled date. /// - public Instant? ScheduledAt { get; } + public Instant? ScheduledAt { get; set; } /// /// The user that has scheduled the content. /// - public RefToken ScheduledBy { get; } + public RefToken ScheduledBy { get; set; } /// /// The date and time when the content item has been created. diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 4dd2a16a4..9ba93a9e9 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -87,10 +87,10 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, this.content = this.content.changeStatus( message.content.scheduledTo || message.content.status, + message.content.scheduledAt, message.content.lastModifiedBy, message.content.version, - message.content.lastModified, - message.content.scheduledAt); + message.content.lastModified); } }); diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index 9589b5b6b..9b610a95d 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -92,25 +92,25 @@
- {{selectionCount}} items selected: + {{selectionCount}} items selected:   - - - - -
- \ No newline at end of file + + + diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index ec262312c..930c9640d 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -52,6 +52,12 @@ export class ContentsPageComponent implements OnDestroy, OnInit { public contentsQuery = ''; public contentsPager = new Pager(0); + public dueTimeDialog = new ModalView(); + public dueTime: string | null = ''; + public dueTimeFunction: Function | null; + public dueTimeAction: string | null = ''; + public dueTimeMode = 'Immediately'; + public selectedItems: { [id: string]: boolean; } = {}; public selectionCount = 0; @@ -118,85 +124,90 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public publishContent(content: ContentDto) { - this.changeContentItem(content, 'publish', 'Published').subscribe(); + this.changeContentItems([content], 'Publish', 'Published', false); } - public publishSelected() { - Observable.forkJoin( - this.contentItems.values - .filter(c => this.selectedItems[c.id]) - .filter(c => c.status !== 'Published') - .map(c => this.changeContentItem(c, 'publish', 'Published'))) - .finally(() => { - this.updateSelectionSummary(); - }) - .subscribe(); + public publishSelected(scheduled: boolean) { + const contents = this.contentItems.filter(c => c.status !== 'Published' && this.selectedItems[c.id]).values; + + this.changeContentItems(contents, 'Publish', 'Published', false); } public unpublishContent(content: ContentDto) { - this.changeContentItem(content, 'unpublish', 'Draft').subscribe(); + this.changeContentItems([content], 'Unpublish', 'Draft', false); } - public unpublishSelected() { - Observable.forkJoin( - this.contentItems.values - .filter(c => this.selectedItems[c.id]) - .filter(c => c.status !== 'Unpublished') - .map(c => this.changeContentItem(c, 'unpublish', 'Draft'))) - .finally(() => { - this.updateSelectionSummary(); - }) - .subscribe(); + public unpublishSelected(scheduled: boolean) { + const contents = this.contentItems.filter(c => c.status === 'Published' && this.selectedItems[c.id]).values; + + this.changeContentItems(contents, 'Unpublish', 'Draft', false); } - private changeContentItem(content: ContentDto, action: string, status: string): Observable { - return this.contentsService.changeContentStatus(this.ctx.appName, this.schema.name, content.id, action, content.version) - .catch(error => { - this.ctx.notifyError(error); + public archiveContent(content: ContentDto) { + this.changeContentItems([content], 'Archive', 'Archived', true); + } - return Observable.throw(error); - }) - .do(dto => { - this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, this.ctx.userToken, dto.version)); + public archiveSelected(scheduled: boolean) { + const contents = this.contentItems.filter(c => this.selectedItems[c.id]).values; - this.emitContentStatusChanged(content); - }); + this.changeContentItems(contents, 'Archive', 'Archived', true); } - public archiveSelected() { - Observable.forkJoin( - this.contentItems.values.filter(c => this.selectedItems[c.id]) - .map(c => this.changeContentItem(c, 'archive', 'Archived'))) - .finally(() => { - this.load(); - }) - .subscribe(); + public restoreContent(content: ContentDto) { + this.changeContentItems([content], 'Restore', 'Draft', true); } - public archiveContent(content: ContentDto) { - this.changeContentItem(content, 'archive', 'Archived') - .finally(() => { - this.load(); - }) - .subscribe(); + public restoreSelected(scheduled: boolean) { + const contents = this.contentItems.filter(c => this.selectedItems[c.id]).values; + + this.changeContentItems(contents, 'Restore', 'Draft', true); } - public restoreSelected() { - Observable.forkJoin( - this.contentItems.values.filter(c => this.selectedItems[c.id]) - .map(c => this.changeContentItem(c, 'restore', 'Draft'))) - .finally(() => { - this.load(); - }) - .subscribe(); + private changeContentItems(contents: ContentDto[], action: string, status: string, reload: boolean) { + if (contents.length === 0) { + return; + } + + this.dueTimeFunction = () => { + if (this.dueTime) { + reload = false; + } + Observable.forkJoin( + contents + .map(c => this.changeContentItem(c, action, status, this.dueTime, reload))) + .finally(() => { + if (reload) { + this.load(); + } else { + this.updateSelectionSummary(); + } + }) + .subscribe(); + }; + + this.dueTimeAction = action; + this.dueTimeDialog.show(); } - public restoreContent(content: ContentDto) { - this.changeContentItem(content, 'restore', 'Draft') - .finally(() => { - this.load(); + private changeContentItem(content: ContentDto, action: string, status: string, dueTime: string | null, reload: boolean): Observable { + return this.contentsService.changeContentStatus(this.ctx.appName, this.schema.name, content.id, action, dueTime, content.version) + .catch(error => { + this.ctx.notifyError(error); + + return Observable.throw(error); }) - .subscribe(); + .do(dto => { + if (!reload) { + const dt = + dueTime ? + DateTime.parseISO_UTC(dueTime) : + null; + + this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, dt, this.ctx.userToken, dto.version)); + + this.emitContentStatusChanged(content); + } + }); } public deleteSelected(content: ContentDto) { @@ -357,5 +368,18 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.contentFields = [{}]; } } + + public confirmStatusChange() { + this.dueTimeFunction!(); + + this.cancelStatusChange(); + } + + public cancelStatusChange() { + this.dueTimeMode = 'Immediately'; + this.dueTimeDialog.hide(); + this.dueTimeFunction = null; + this.dueTime = null; + } } diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index ca5a19fa4..112827d57 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ContentDto, DateTime } from 'shared'; +import { ContentDto } from 'shared'; export class ContentCreated { constructor( diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index e3bb20cdf..bfff66cfc 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -10,7 +10,26 @@ - + + + + + +
+ {{content.status}} +
+
+ + + + + + +
+ Will be set {{content.scheduledTo}} at {{content.scheduledAt | sqxFullDateTime}} +
+
+ {{content.lastModified | sqxFromNow}} diff --git a/src/Squidex/app/features/content/shared/content-item.component.scss b/src/Squidex/app/features/content/shared/content-item.component.scss index 727784a9c..8b360453e 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.scss +++ b/src/Squidex/app/features/content/shared/content-item.component.scss @@ -1,22 +1,31 @@ @import '_vars'; @import '_mixins'; -.content { +.content-status { & { + vertical-align: middle; cursor: pointer; } &-published { - & { - @include circle(.5rem); - display: inline-block; - border: 0; - background: $color-theme-green; - margin-left: .4rem; - } + color: $color-theme-green; + } + + &-draft { + color: $color-text-decent; + } + + &-archived { + color: $color-theme-error; + } - &.unpublished { - background: $color-theme-error; - } + &-tooltip { + @include border-radius; + background: $color-tooltip; + border: 0; + font-size: .9rem; + font-weight: normal; + color: $color-dark-foreground; + padding: .75rem; } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content-item.component.ts b/src/Squidex/app/features/content/shared/content-item.component.ts index 7ba32c2f3..715f445d9 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.ts +++ b/src/Squidex/app/features/content/shared/content-item.component.ts @@ -46,7 +46,7 @@ export class ContentItemComponent implements OnInit, OnChanges { public deleting = new EventEmitter(); @Output() - public selectedChange = new EventEmitter(); + public selectedChange = new EventEmitter(); @Input() public selected = false; @@ -74,6 +74,8 @@ export class ContentItemComponent implements OnInit, OnChanges { public dropdown = new ModalView(false, true); + public scheduleTooltip = new ModalView(false, true); + public values: any[] = []; constructor(public readonly ctx: AppContext diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html index b0cf8866b..7af9fc272 100644 --- a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html @@ -77,7 +77,7 @@ Attempts: {{event.numCalls}}
- Next: {{event.nextAttempt.toStringFormat('MMM DD h:mm:ss a')}} + Next: {{event.nextAttempt | sqxFromNow}}
-
+
diff --git a/src/Squidex/app/framework/angular/date-time-editor.component.ts b/src/Squidex/app/framework/angular/date-time-editor.component.ts index 3ee9fc078..ad6103174 100644 --- a/src/Squidex/app/framework/angular/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/date-time-editor.component.ts @@ -40,6 +40,9 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, @Input() public enforceTime: boolean; + @Input() + public hideClear: boolean; + public timeControl = new FormControl(); public dateControl = new FormControl(); @@ -169,7 +172,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, let result: string | null = null; if ((this.dateValue && !this.dateValue.isValid()) || (this.timeValue && !this.timeValue.isValid())) { - result = 'Invalid DateTime'; + result = null; } else if (!this.dateValue && !this.timeValue) { result = null; } else { diff --git a/src/Squidex/app/framework/angular/date-time.pipes.spec.ts b/src/Squidex/app/framework/angular/date-time.pipes.spec.ts index f738a3d35..c01bb8fec 100644 --- a/src/Squidex/app/framework/angular/date-time.pipes.spec.ts +++ b/src/Squidex/app/framework/angular/date-time.pipes.spec.ts @@ -13,6 +13,7 @@ import { DayPipe, DurationPipe, FromNowPipe, + FullDateTimePipe, MonthPipe, ShortDatePipe, ShortTimePipe @@ -33,6 +34,17 @@ describe('DurationPipe', () => { }); }); +describe('FullDateTimePipe', () => { + it('should format to nice string', () => { + const pipe = new FullDateTimePipe(); + + const actual = pipe.transform(dateTime); + const expected = 'Thursday, October 3, 2013 12:13 PM'; + + expect(actual).toBe(expected); + }); +}); + describe('DayPipe', () => { it('should format to day numbers', () => { const pipe = new DayPipe(); diff --git a/src/Squidex/app/framework/angular/date-time.pipes.ts b/src/Squidex/app/framework/angular/date-time.pipes.ts index 8c0d9d9e6..754f278ad 100644 --- a/src/Squidex/app/framework/angular/date-time.pipes.ts +++ b/src/Squidex/app/framework/angular/date-time.pipes.ts @@ -80,6 +80,16 @@ export class ShortTimePipe implements PipeTransform { } } +@Pipe({ + name: 'sqxFullDateTime', + pure: true +}) +export class FullDateTimePipe implements PipeTransform { + public transform(value: DateTime): any { + return value.toStringFormat('LLLL'); + } +} + @Pipe({ name: 'sqxDuration', pure: true diff --git a/src/Squidex/app/framework/angular/dialog-renderer.component.scss b/src/Squidex/app/framework/angular/dialog-renderer.component.scss index a990f3756..f86958242 100644 --- a/src/Squidex/app/framework/angular/dialog-renderer.component.scss +++ b/src/Squidex/app/framework/angular/dialog-renderer.component.scss @@ -29,8 +29,4 @@ &-bottomleft { @include fixed(auto, auto, 0, 0); } -} - -.clearfix { - width: 100%; } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/onboarding-tooltip.component.html b/src/Squidex/app/framework/angular/onboarding-tooltip.component.html index 4de8bb7f8..bd217d082 100644 --- a/src/Squidex/app/framework/angular/onboarding-tooltip.component.html +++ b/src/Squidex/app/framework/angular/onboarding-tooltip.component.html @@ -1,5 +1,4 @@
-
diff --git a/src/Squidex/app/framework/angular/onboarding-tooltip.component.scss b/src/Squidex/app/framework/angular/onboarding-tooltip.component.scss index 33430c7e2..2b991d2d7 100644 --- a/src/Squidex/app/framework/angular/onboarding-tooltip.component.scss +++ b/src/Squidex/app/framework/angular/onboarding-tooltip.component.scss @@ -1,12 +1,10 @@ @import '_mixins'; @import '_vars'; -$color: #1a2129; - .onboarding { &-help { @include border-radius; - background: $color; + background: $color-tooltip; border: 0; max-width: 20rem; padding: .75rem; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 45b1b528e..9a4f4b39f 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -32,6 +32,7 @@ import { FileSizePipe, FocusOnInitDirective, FromNowPipe, + FullDateTimePipe, IgnoreScrollbarDirective, ImageSourceDirective, IndeterminateValueDirective, @@ -98,6 +99,7 @@ import { FileSizePipe, FocusOnInitDirective, FromNowPipe, + FullDateTimePipe, IgnoreScrollbarDirective, ImageSourceDirective, IndeterminateValueDirective, @@ -148,6 +150,7 @@ import { FileSizePipe, FocusOnInitDirective, FromNowPipe, + FullDateTimePipe, IgnoreScrollbarDirective, ImageSourceDirective, IndeterminateValueDirective, diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index af7b1dc45..7f6637a95 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -39,7 +39,7 @@ describe('ContentDto', () => { it('should update status property and user info when changing status', () => { const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version); - const content_2 = content_1.changeStatus('Published', modifier, newVersion, modified); + const content_2 = content_1.changeStatus('Published', null, modifier, newVersion, modified); expect(content_2.status).toEqual('Published'); expect(content_2.lastModified).toEqual(modified); @@ -49,7 +49,7 @@ describe('ContentDto', () => { it('should update schedules property and user info when changing status with due time', () => { const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version); - const content_2 = content_1.changeStatus('Published', modifier, newVersion, modified, dueTime); + const content_2 = content_1.changeStatus('Published', dueTime, modifier, newVersion, modified); expect(content_2.status).toEqual('Draft'); expect(content_2.lastModified).toEqual(modified); @@ -315,7 +315,7 @@ describe('ContentsService', () => { it('should make put request to change content status', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', version).subscribe(); + contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', null, version).subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/publish'); @@ -328,11 +328,11 @@ describe('ContentsService', () => { it('should make put request with due time when status change is scheduled', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - const dueTime = DateTime.parseISO_UTC('2016-12-12T10:10'); + const dueTime = '2016-12-12T10:10:00'; - contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', version, dueTime).subscribe(); + contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', dueTime, version).subscribe(); - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/publish?dueTime=2016-12-12T10:10:00.000Z'); + const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/publish?dueTime=2016-12-12T10:10:00'); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toEqual(version.value); diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 3b87d6874..966c90414 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -59,7 +59,7 @@ export class ContentDto { this.version); } - public changeStatus(status: string, user: string, version: Version, now?: DateTime, dueTime: DateTime | null = null): ContentDto { + public changeStatus(status: string, dueTime: DateTime | null, user: string, version: Version, now?: DateTime): ContentDto { if (dueTime) { return new ContentDto( this.id, @@ -249,11 +249,11 @@ export class ContentsService { .pretifyError('Failed to delete content. Please reload.'); } - public changeContentStatus(appName: string, schemaName: string, id: string, action: string, version: Version, dueTime?: DateTime): Observable> { + public changeContentStatus(appName: string, schemaName: string, id: string, action: string, dueTime: string | null, version: Version): Observable> { let url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${action}`); if (dueTime) { - url += `?dueTime=${dueTime.toISOString()}`; + url += `?dueTime=${dueTime}`; } return HTTP.putVersioned(this.http, url, {}, version) diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index c22e92ab3..b7993ea66 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -458,6 +458,12 @@ a { margin-top: 4.5rem; } } + + &-footer { + .clearfix { + width: 100%; + } + } } // diff --git a/src/Squidex/app/theme/_lists.scss b/src/Squidex/app/theme/_lists.scss index 717491867..754191386 100644 --- a/src/Squidex/app/theme/_lists.scss +++ b/src/Squidex/app/theme/_lists.scss @@ -27,6 +27,10 @@ } } + td { + border-top: 0; + } + thead { // Small font size for the table header, content is more important! th { diff --git a/src/Squidex/app/theme/_panels.scss b/src/Squidex/app/theme/_panels.scss index 33bb19aa0..b1b0b88d3 100644 --- a/src/Squidex/app/theme/_panels.scss +++ b/src/Squidex/app/theme/_panels.scss @@ -289,15 +289,24 @@ .grid-content { @include flex-grow(1); - margin: 0; - margin-top: .25rem; + padding-top: .5rem; + padding-bottom: .5rem; overflow-y: scroll; } .grid-header { - padding-top: .75 * $panel-padding; - border: 0; - border-bottom: 2px solid $color-border; + & { + border: 0; + border-bottom: 2px solid $color-border; + } + + th { + padding: .7rem; + } + + .table-items { + margin: 0; + } } .grid-footer { @@ -305,6 +314,6 @@ } .pagination { - margin-top: .25rem; + margin: .25rem 0; } } \ No newline at end of file diff --git a/src/Squidex/app/theme/_vars.scss b/src/Squidex/app/theme/_vars.scss index 9dd26ff4b..a4862968b 100644 --- a/src/Squidex/app/theme/_vars.scss +++ b/src/Squidex/app/theme/_vars.scss @@ -6,6 +6,7 @@ $color-border-dark: #b3bbbf; $color-title: #000; $color-text: #373a3c; $color-text-decent: #a9b2bb; +$color-tooltip: #1a2129; $color-input: #dbe4eb; $color-input-background: #fff; diff --git a/src/Squidex/app/theme/icomoon/demo-files/demo.css b/src/Squidex/app/theme/icomoon/demo-files/demo.css index 38755ccf3..1c4674f67 100644 --- a/src/Squidex/app/theme/icomoon/demo-files/demo.css +++ b/src/Squidex/app/theme/icomoon/demo-files/demo.css @@ -150,10 +150,10 @@ p { font-size: 32px; } .fs2 { - font-size: 28px; + font-size: 32px; } .fs3 { - font-size: 32px; + font-size: 28px; } .fs4 { font-size: 32px; diff --git a/src/Squidex/app/theme/icomoon/demo.html b/src/Squidex/app/theme/icomoon/demo.html index 78d96da2d..810b7602e 100644 --- a/src/Squidex/app/theme/icomoon/demo.html +++ b/src/Squidex/app/theme/icomoon/demo.html @@ -9,10 +9,26 @@
-

Font Name: icomoon (Glyphs: 83)

+

Font Name: icomoon (Glyphs: 85)

Grid Size: Unknown

+
+
+ + + + icon-circle +
+
+ + +
+
+ liga: + +
+
@@ -847,17 +863,17 @@
-

Grid Size: 14

+

Grid Size: 16

- + - icon-action-Slack + icon-clock
- - + +
liga: @@ -866,14 +882,14 @@
- + - icon-orleans + icon-bin2
- - + +
liga: @@ -882,30 +898,30 @@
- + - icon-document-lock + icon-earth
- - + +
liga: - +
- + - icon-document-unpublish + icon-elapsed
- - + +
liga: @@ -914,14 +930,14 @@
- + - icon-angle-down + icon-google
- - + +
liga: @@ -930,14 +946,14 @@
- + - icon-angle-left + icon-lock
- - + +
liga: @@ -946,14 +962,14 @@
- + - icon-angle-right + icon-microsoft
- - + +
liga: @@ -962,14 +978,14 @@
- + - icon-angle-up + icon-action-AzureQueue
- - + +
liga: @@ -978,14 +994,14 @@
- + - icon-api + icon-pause
- - + +
liga: @@ -994,14 +1010,14 @@
- + - icon-assets + icon-play
- - + +
liga: @@ -1010,14 +1026,14 @@
- + - icon-bug + icon-reset
- - + +
liga: @@ -1026,14 +1042,14 @@
- + - icon-caret-down + icon-settings2
- - + +
liga: @@ -1042,14 +1058,14 @@
- + - icon-caret-left + icon-timeout
- - + +
liga: @@ -1058,209 +1074,209 @@
- + - icon-caret-right + icon-unlocked
- - + +
liga:
-
+
+
+

Grid Size: 14

+
- + - icon-caret-up + icon-action-Slack
- - + +
liga:
-
+
- + - icon-contents + icon-orleans
- - + +
liga:
-
+
- + - icon-trigger-ContentChanged + icon-document-lock
- - + +
liga:
-
+
- + - icon-control-Date + icon-document-unpublish
- - + +
liga:
-
+
- + - icon-control-DateTime + icon-angle-down
- - + +
liga:
-
+
- + - icon-control-Markdown + icon-angle-left
- - + +
liga:
-
+
- + - icon-grid + icon-angle-right
- - + +
liga:
-
+
- + - icon-list + icon-angle-up
- - + +
liga:
-
+
- + - icon-user-o + icon-api
- - + +
liga:
-
+
- + - icon-rules + icon-assets
- - + +
liga:
-
+
- + - icon-action-Webhook + icon-bug
- - + +
liga:
-
-
-

Grid Size: 16

- + - icon-bin2 + icon-caret-down
- - + +
liga: @@ -1269,30 +1285,30 @@
- + - icon-earth + icon-caret-left
- - + +
liga: - +
- + - icon-elapsed + icon-caret-right
- - + +
liga: @@ -1301,14 +1317,14 @@
- + - icon-google + icon-caret-up
- - + +
liga: @@ -1317,14 +1333,14 @@
- + - icon-lock + icon-contents
- - + +
liga: @@ -1333,14 +1349,14 @@
- + - icon-microsoft + icon-trigger-ContentChanged
- - + +
liga: @@ -1349,14 +1365,14 @@
- + - icon-action-AzureQueue + icon-control-Date
- - + +
liga: @@ -1365,14 +1381,14 @@
- + - icon-pause + icon-control-DateTime
- - + +
liga: @@ -1381,14 +1397,14 @@
- + - icon-play + icon-control-Markdown
- - + +
liga: @@ -1397,14 +1413,14 @@
- + - icon-reset + icon-grid
- - + +
liga: @@ -1413,14 +1429,14 @@
- + - icon-settings2 + icon-list
- - + +
liga: @@ -1429,14 +1445,14 @@
- + - icon-timeout + icon-user-o
- - + +
liga: @@ -1445,14 +1461,30 @@
- + - icon-unlocked + icon-rules
- - + + +
+
+ liga: + +
+
+
+
+ + + + icon-action-Webhook +
+
+ +
liga: diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot index c3b9462c50274c8fdad142ce201e6e366f870ec5..e8abe5415815252076e027ac437dcffc7cd7d46c 100644 GIT binary patch delta 431 zcmeyenQ_53MmB|L28NobiEL)f{d+_v+E)nfVqjp{1H=i*xrqh0b#9%WctWOL*7u+t z0|Vm(28NIg8L5dWg3H+UGcbfX0M(gg00lUiSfhaa3Lsx4Be$f&mfXaO0)_%cp!$d{K)ym=Vs7e8FV5dU{uQA5wu1cP5(Z|F(x@1a zJOeW` zU`Rui0^J6;P8!G&VdrCHXH%YB;3vYw!T{7O!Nhm~XpJ}{!{)M}aAs+dJ^k_gHeVUI zSwNm)xVHGAJdB?FBGjBQaXK8k;mDv_!NbKz1-hGDa|nfs`^ZO@0_D KyxBiuDI)*{O?B=7 delta 260 zcmZ3mjq%H7Mz$}J3=9F06WPp|T^(0Vw674{$-uy{2Z$4ra}x`0Yu!3J@q|phUyMf+ z0|Vm(28Q4X8L5dWf|FjSGcbfI0M(gg00lUiSR;UZAZAd>$StXG_$T-r$gctFiOI=N zPV|nAyTrf{b^|ECAvdw2fFX}Dih&`#1ISm%OUzB3>BadQ$X@|e-&T-cT*AN%lwpW8 z0Le2jGcz8WoWj`6=s)=bqcUrkfH!~dW(lUmfXy?5LztykIl9L4+k9o1A&1tdzo diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg index e3075362d..313a70455 100644 --- a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg +++ b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg @@ -87,6 +87,8 @@ + + diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf index d71543c96b753da153ff1f74125fe0119087cd34..d4f4fe27a0e8fc54a1f482f3a888412d5fbe5b4e 100644 GIT binary patch delta 458 zcmcbxiSf@C#(D-u1_lOhh6V;^1_S?KeItG$-d#YEJwTk0oSRs1Tj$nk1_nkMAU`KP zvA6(83jp~YK$;^xr!q~}_n;k+KY@WEWJ5-3Vv680w*3qYVGclfvkagBClhNFkY54h zt7PPsRCxRod=BK#0qRM~$xn7nEScX?!oUy#lxNtIn^;l6P{0V(5U~ZwSIA4uO`Yk* z`5VZ;0@Tn}kY8K^bSMx+#en1)n3<1HJkZS;IN66$nYBm2m%o2=31eb_1W?WY11t>8 z2N+lx6d7z87*y2-jYY+oC$IOFo*d>Z!^SlEa>LrUlOM8)D`W+@x@F1%L8hB)022H1 zB}VC|>zF1_-W?=q(10NgRSI+#+%9P#M}(b^k)2I>@}gStyhU#A3qQmN0MlxE8~^|S delta 285 zcmeyfh4I2B#(D-u1_lOhh6V;^1_S?KeItG$-km^^JwTk0oSRs1TkFvRT&Pz9j8Sq4ymlZiC~$OmEu zm5khy3WtA!&w>0Jpq`kV{A9;O@7TCY3=Cm6fC@I`CRP+MQnO`Yk* z`5VYz0o2e|kY8K^bSMx+8i3>(n3<1FJkZVPKiP*-nYBy6o4@uI=4=z zCl-Umt^oNQP%Mz1Q<(-7yTiZ`vH^r;eGl4Yq$Z{?Fog90)tG^>;4-%T89+gx*byLK z1%x@7Sfes>ODce3FM#@i>KRi!{s}(M$xjBV^Na8SD%t|VCG$H0LU16j<>$0zeJb~6S}p24Wh+9Tl0-@o|) zV`6{=Q1br)76#@646F=_47Lmms_KHqqTE;@M#J+rqQTpjRrpX!_-Epy5S+Q~58bE+x|7pO`0@V(5Gu(b@AV-9qkCB~C zd9tI0hztt@vKh#1#seVdFfweG3kqkJ7TMDu&u{aUftv;BPX>l-iyz9v=*bg8%^4#% gp9qa$oGcN(oxe$AlV*gLh*ktR#v?c13O~dM0NIa*Bme*a delta 341 zcmX@Hm2u8yMv-!VH#Y`G1|W!>!@v!u114{b5}&9oQol1fH?e?$fiVLpoCCtQwQe0v zPb>zBT>cmfFf#dtJjq$Z{?Foc5inSrq2q}S;gKtZ6`5+Gj% zggKd5BQkPJDu7}qfcj%V*x{ex^PK!-pgO;BkfIGB>>V3-DL1hKXmL2mVg)eHV~om6 z%uNM~eF1801L2uooWBe5i%WpMse!9w2C|r$k4@%b>}K?zJcCi0wM)R8zjyNi#>9Zl zj=>?!(yJU@ Date: Sat, 10 Feb 2018 12:55:43 +0100 Subject: [PATCH 20/26] UI improved. --- .../contents/contents-page.component.html | 8 +-- .../shared/content-item.component.html | 15 ++--- .../shared/content-item.component.scss | 1 - .../content/shared/content-item.component.ts | 7 +- .../schemas/pages/schema/field.component.html | 2 +- .../pages/schema/schema-page.component.html | 2 +- .../angular/control-errors.component.ts | 5 +- .../angular/modal-target.directive.ts | 4 ++ .../angular/onboarding-tooltip.component.html | 2 +- .../framework/angular/tooltip.component.scss | 12 ++++ .../framework/angular/tooltip.component.ts | 64 +++++++++++++++++++ .../angular/user-report.component.ts | 6 +- src/Squidex/app/framework/declarations.ts | 1 + src/Squidex/app/framework/module.ts | 3 + .../language-selector.component.html | 2 +- .../pages/internal/apps-menu.component.html | 2 +- .../internal/profile-menu.component.html | 2 +- 17 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 src/Squidex/app/framework/angular/tooltip.component.scss create mode 100644 src/Squidex/app/framework/angular/tooltip.component.ts diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index 9b610a95d..f00ddc40a 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -28,7 +28,7 @@ Search for content using full text search over all fields and languages! -