mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
58 changed files with 1125 additions and 740 deletions
@ -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<T> : DomainObjectBase<T> where T : IDomainState, new() |
|||
{ |
|||
public override void RaiseEvent(Envelope<IEvent> @event) |
|||
{ |
|||
if (@event.Payload is AppEvent appEvent) |
|||
{ |
|||
@event.SetAppId(appEvent.AppId.Id); |
|||
} |
|||
|
|||
base.RaiseEvent(@event); |
|||
} |
|||
} |
|||
} |
|||
@ -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<T> SetAppId<T>(this Envelope<T> envelope, Guid value) where T : class |
|||
{ |
|||
envelope.Headers.Set(SquidexHeaders.AppId, value); |
|||
|
|||
return envelope; |
|||
} |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
// ==========================================================================
|
|||
// 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.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 readonly ConcurrentDictionary<string, bool> projections = new ConcurrentDictionary<string, bool>(); |
|||
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 CreateFilterProjectionName(string filter) |
|||
{ |
|||
return $"by-{prefix.Slugify()}-{filter.Slugify()}"; |
|||
} |
|||
|
|||
private string CreatePropertyProjectionName(string property) |
|||
{ |
|||
return $"by-{prefix.Slugify()}-{property.Slugify()}-property"; |
|||
} |
|||
|
|||
public async Task<string> CreateProjectionAsync(string property, object value) |
|||
{ |
|||
var name = CreatePropertyProjectionName(property); |
|||
|
|||
var query = |
|||
$@"fromAll()
|
|||
.when({{ |
|||
$any: function (s, e) {{ |
|||
if (e.streamId.indexOf('{prefix}') === 0 && e.metadata.{property}) {{ |
|||
linkTo('{name}-' + e.metadata.{property}, e); |
|||
}} |
|||
}} |
|||
}});";
|
|||
|
|||
await CreateProjectionAsync(name, query); |
|||
|
|||
return $"{name}-{value}"; |
|||
} |
|||
|
|||
public async Task<string> CreateProjectionAsync(string streamFilter = null) |
|||
{ |
|||
streamFilter = streamFilter ?? ".*"; |
|||
|
|||
var name = CreateFilterProjectionName(streamFilter); |
|||
|
|||
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(name, query, credentials); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (!ex.Is<ProjectionCommandConflictException>()) |
|||
{ |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -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<string, bool> SubscriptionsCreated = new ConcurrentDictionary<string, bool>(); |
|||
|
|||
private static string ParseFilter(string prefix, string filter) |
|||
{ |
|||
return string.Format(CultureInfo.InvariantCulture, ProjectionName, prefix.Slugify(), filter.Slugify()); |
|||
} |
|||
|
|||
public static async Task<string> 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<ProjectionCommandConflictException>()) |
|||
{ |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return streamName; |
|||
} |
|||
|
|||
public static async Task<ProjectionsManager> 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; |
|||
} |
|||
} |
|||
} |
|||
@ -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<MongoEventCommit>, 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<IReadOnlyList<StoredEvent>> 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<StoredEvent>(); |
|||
|
|||
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<StoredEvent, Task> 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<StoredEvent, Task> 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<StoredEvent, Task> callback, StreamPosition lastPosition, FilterDefinition<MongoEventCommit> 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<MongoEventCommit> CreateFilter(string property, object value, StreamPosition streamPosition) |
|||
{ |
|||
var filters = new List<FilterDefinition<MongoEventCommit>>(); |
|||
|
|||
AddPositionFilter(streamPosition, filters); |
|||
AddPropertyFitler(property, value, filters); |
|||
|
|||
return Filter.And(filters); |
|||
} |
|||
|
|||
private static FilterDefinition<MongoEventCommit> CreateFilter(string streamFilter, StreamPosition streamPosition) |
|||
{ |
|||
var filters = new List<FilterDefinition<MongoEventCommit>>(); |
|||
|
|||
AddPositionFilter(streamPosition, filters); |
|||
AddStreamFilter(streamFilter, filters); |
|||
|
|||
return Filter.And(filters); |
|||
} |
|||
|
|||
private static void AddPropertyFitler(string property, object value, List<FilterDefinition<MongoEventCommit>> filters) |
|||
{ |
|||
filters.Add(Filter.Eq(CreateIndexPath(property), value)); |
|||
} |
|||
|
|||
private static void AddStreamFilter(string streamFilter, List<FilterDefinition<MongoEventCommit>> 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<FilterDefinition<MongoEventCommit>> 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.Metadata.{property}"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
// ==========================================================================
|
|||
// 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<EventData> events) |
|||
{ |
|||
return AppendAsync(commitId, streamName, EtagVersion.Any, events); |
|||
} |
|||
|
|||
public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events) |
|||
{ |
|||
Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); |
|||
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<long> GetEventStreamOffset(string streamName) |
|||
{ |
|||
var document = |
|||
await Collection.Find(Filter.Eq(EventStreamField, streamName)) |
|||
.Project<BsonDocument>(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<EventData> events) |
|||
{ |
|||
var commitEvents = new MongoEvent[events.Count]; |
|||
|
|||
var i = 0; |
|||
|
|||
foreach (var e in events) |
|||
{ |
|||
var mongoEvent = MongoEvent.FromEventData(e); |
|||
|
|||
commitEvents[i++] = mongoEvent; |
|||
} |
|||
|
|||
var mongoCommit = new MongoEventCommit |
|||
{ |
|||
Id = commitId, |
|||
Events = commitEvents, |
|||
EventsCount = events.Count, |
|||
EventStream = streamName, |
|||
EventStreamOffset = expectedVersion, |
|||
Timestamp = EmptyTimestamp |
|||
}; |
|||
|
|||
return mongoCommit; |
|||
} |
|||
} |
|||
} |
|||
@ -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<T> : ClassSerializerBase<T> where T : JToken |
|||
{ |
|||
public static readonly JTokenSerializer<T> Instance = new JTokenSerializer<T>(); |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -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<IMigration> Migrations) GetNext(int version); |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
// ==========================================================================
|
|||
// 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<IMigration> previousMigrations) |
|||
{ |
|||
if (!previousMigrations.Any(x => x is Migration01_FromCqrs)) |
|||
{ |
|||
await rebuilder.RebuildConfigAsync(); |
|||
await rebuilder.RebuildContentAsync(); |
|||
await rebuilder.RebuildAssetsAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
// ==========================================================================
|
|||
// 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 sealed class MigrationPath : IMigrationPath |
|||
{ |
|||
private const int CurrentVersion = 6; |
|||
private readonly IServiceProvider serviceProvider; |
|||
|
|||
public MigrationPath(IServiceProvider serviceProvider) |
|||
{ |
|||
this.serviceProvider = serviceProvider; |
|||
} |
|||
|
|||
public (int Version, IEnumerable<IMigration> Migrations) GetNext(int version) |
|||
{ |
|||
if (version == CurrentVersion) |
|||
{ |
|||
return (CurrentVersion, null); |
|||
} |
|||
|
|||
var migrations = new List<IMigration>(); |
|||
|
|||
// Version 6: Convert Event store. Must always be executed first.
|
|||
if (version < 6) |
|||
{ |
|||
migrations.Add(serviceProvider.GetRequiredService<ConvertEventStore>()); |
|||
} |
|||
|
|||
// Version 5: Fixes the broken command architecture and requires a rebuild of all snapshots.
|
|||
if (version < 5) |
|||
{ |
|||
migrations.Add(serviceProvider.GetRequiredService<RebuildSnapshots>()); |
|||
} |
|||
|
|||
// Version 1: Introduce App patterns.
|
|||
if (version <= 1) |
|||
{ |
|||
migrations.Add(serviceProvider.GetRequiredService<AddPatterns>()); |
|||
} |
|||
|
|||
return (CurrentVersion, migrations); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// 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 |
|||
{ |
|||
public sealed class ConvertEventStore : IMigration |
|||
{ |
|||
private readonly IEventStore eventStore; |
|||
|
|||
public ConvertEventStore(IEventStore eventStore) |
|||
{ |
|||
this.eventStore = eventStore; |
|||
} |
|||
|
|||
public async Task UpdateAsync() |
|||
{ |
|||
if (eventStore is MongoEventStore mongoEventStore) |
|||
{ |
|||
var collection = mongoEventStore.RawCollection; |
|||
|
|||
var filter = Builders<BsonDocument>.Filter; |
|||
|
|||
await collection.Find(new BsonDocument()).ForEachAsync(async commit => |
|||
{ |
|||
foreach (BsonDocument @event in commit["Events"].AsBsonArray) |
|||
{ |
|||
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<Guid>.Parse(appId.ToString(), Guid.TryParse).Id; |
|||
} |
|||
|
|||
@event.Remove("EventId"); |
|||
@event["Metadata"] = meta.ToBson(); |
|||
} |
|||
|
|||
await collection.ReplaceOneAsync(filter.Eq("_id", commit["_id"].AsString), commit); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue