Browse Source

Merge branch 'master' into test

pull/356/head
Sebastian Stehle 7 years ago
parent
commit
d32a894359
  1. 2
      extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
  2. 2
      extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  3. 4
      src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  4. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  5. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  6. 32
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs
  7. 10
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs
  8. 4
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  9. 56
      src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs
  11. 4
      src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs
  12. 14
      src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs
  13. 5
      src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs
  14. 46
      src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
  15. 7
      src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs
  16. 11
      src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs
  17. 708
      src/Squidex.Infrastructure/StringExtensions.cs
  18. 23
      src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  19. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs
  20. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs
  21. 11
      src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html
  22. 3
      src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts
  23. 11
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html
  24. 3
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts
  25. 4
      src/Squidex/app/framework/angular/forms/checkbox-group.component.html
  26. 8
      src/Squidex/app/framework/angular/forms/checkbox-group.component.scss
  27. 13
      src/Squidex/app/framework/angular/forms/control-errors.component.ts
  28. 6
      src/Squidex/app/framework/angular/forms/error-formatting.spec.ts
  29. 1
      src/Squidex/app/framework/angular/forms/error-formatting.ts
  30. 34
      src/Squidex/app/framework/angular/forms/validators.spec.ts
  31. 23
      src/Squidex/app/framework/angular/forms/validators.ts
  32. 2
      src/Squidex/app/shared/services/schemas.types.ts
  33. 4
      src/Squidex/app/shared/state/contents.forms.spec.ts
  34. 8
      src/Squidex/app/shared/state/contents.forms.ts
  35. 6
      src/Squidex/appsettings.json
  36. 59
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  37. 21
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs
  38. 61
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs
  39. 11
      tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs
  40. 2
      tools/Migrate_00/Program.cs
  41. 3
      tools/Migrate_01/OldEvents/AssetRenamed.cs
  42. 3
      tools/Migrate_01/OldEvents/AssetTagged.cs
  43. 2
      tools/Migrate_01/RebuildOptions.cs
  44. 11
      tools/Migrate_01/RebuildRunner.cs

2
extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs

@ -15,7 +15,7 @@ namespace Squidex.Extensions.Actions.Algolia
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16 .842C7.633.842.842 7.625.842 16S7.625 31.158 16 31.158c8.374 0 15.158-6.791 15.158-15.166S24.375.842 16 .842zm0 25.83c-5.898 0-10.68-4.781-10.68-10.68S10.101 5.313 16 5.313s10.68 4.781 10.68 10.679-4.781 10.68-10.68 10.68zm0-19.156v7.956c0 .233.249.388.458.279l7.055-3.663a.312.312 0 0 0 .124-.434 8.807 8.807 0 0 0-7.319-4.447z'/></svg>",
IconColor = "#0d9bf9",
Display = "Populate Algolia index",
Description = "Populate and synchronize indices in Algolia for full text search.",
Description = "Populate and synchronize indexes in Algolia for full text search.",
ReadMore = "https://www.algolia.com/")]
public sealed class AlgoliaAction : RuleAction
{

2
extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs

@ -17,7 +17,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 28'><path d='M13.427 17.436H4.163C3.827 16.354 3.636 15.2 3.636 14s.182-2.355.527-3.436h15.245c1.891 0 3.418 1.545 3.418 3.445a3.421 3.421 0 0 1-3.418 3.427h-5.982zm-.436 1.146H4.6a11.508 11.508 0 0 0 4.2 4.982 11.443 11.443 0 0 0 15.827-3.209 5.793 5.793 0 0 0-4.173-1.773H12.99zm7.464-9.164a5.794 5.794 0 0 0 4.173-1.773 11.45 11.45 0 0 0-9.536-5.1c-2.327 0-4.491.7-6.3 1.891a11.554 11.554 0 0 0-4.2 4.982h15.864z'/></svg>",
IconColor = "#1e5470",
Display = "Populate ElasticSearch index",
Description = "Populate and synchronize indices in ElasticSearch for full text search.",
Description = "Populate and synchronize indexes in ElasticSearch for full text search.",
ReadMore = "https://www.elastic.co/")]
public sealed class ElasticSearchAction : RuleAction
{

4
src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs

@ -13,12 +13,12 @@ namespace Squidex.Domain.Apps.Core.Contents
public sealed class NamedContentData : ContentData<string>, IEquatable<NamedContentData>
{
public NamedContentData()
: base(StringComparer.OrdinalIgnoreCase)
: base(StringComparer.Ordinal)
{
}
public NamedContentData(int capacity)
: base(capacity, StringComparer.OrdinalIgnoreCase)
: base(capacity, StringComparer.Ordinal)
{
}

2
src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs

@ -33,6 +33,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? AspectHeight { get; set; }
public bool AllowDuplicates { get; set; }
public ReadOnlyCollection<string> AllowedExtensions { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

2
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxItems { get; set; }
public bool AllowDuplicates { get; set; }
public Guid SchemaId { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

32
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class UniqueValuesValidator<T> : IValidator
{
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is IEnumerable<T> items && items.Any())
{
var itemsArray = items.ToArray();
if (itemsArray.Length != itemsArray.Distinct().Count())
{
addError(context.Path, "Must not contain duplicate values.");
}
}
return TaskHelper.Done;
}
}
}

10
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs

@ -55,6 +55,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
if (!field.Properties.AllowDuplicates)
{
yield return new UniqueValuesValidator<Guid>();
}
yield return new AssetsValidator(field.Properties);
}
@ -125,6 +130,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
if (!field.Properties.AllowDuplicates)
{
yield return new UniqueValuesValidator<Guid>();
}
if (field.Properties.SchemaId != Guid.Empty)
{
yield return new ReferencesValidator(field.Properties.SchemaId);

4
src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs

@ -131,11 +131,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
}
}
public override async Task CleanupRestoreAsync(Guid appId)
public override async Task CleanupRestoreErrorAsync(Guid appId)
{
if (isReserved)
{
await appsByNameIndex.ReserveAppAsync(appId, appName);
await appsByNameIndex.RemoveReservationAsync(appId, appName);
}
}

56
src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using Orleans.Concurrency;
using Squidex.Domain.Apps.Entities.Backup.Helpers;
@ -34,8 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
private readonly IAssetStore assetStore;
private readonly IBackupArchiveLocation backupArchiveLocation;
private readonly IClock clock;
private readonly IEnumerable<BackupHandler> handlers;
private readonly IJsonSerializer serializer;
private readonly IServiceProvider serviceProvider;
private readonly IEventDataFormatter eventDataFormatter;
private readonly IEventStore eventStore;
private readonly ISemanticLog log;
@ -48,8 +49,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
IClock clock,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
IEnumerable<BackupHandler> handlers,
IJsonSerializer serializer,
IServiceProvider serviceProvider,
ISemanticLog log,
IStore<Guid> store)
: base(store)
@ -59,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
Guard.NotNull(clock, nameof(clock));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(handlers, nameof(handlers));
Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(log, nameof(log));
@ -68,8 +69,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
this.clock = clock;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.handlers = handlers;
this.serializer = serializer;
this.serviceProvider = serviceProvider;
this.log = log;
}
@ -86,10 +87,12 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
if (!job.Stopped.HasValue)
{
var jobId = job.Id.ToString();
job.Stopped = clock.GetCurrentInstant();
await Safe.DeleteAsync(backupArchiveLocation, job.Id, log);
await Safe.DeleteAsync(assetStore, job.Id, log);
await Safe.DeleteAsync(backupArchiveLocation, jobId, log);
await Safe.DeleteAsync(assetStore, jobId, log);
job.Status = JobStatus.Failed;
@ -120,15 +123,29 @@ namespace Squidex.Domain.Apps.Entities.Backup
currentTask = new CancellationTokenSource();
currentJob = job;
var lastTimestamp = job.Started;
State.Jobs.Insert(0, job);
await WriteStateAsync();
Process(job, currentTask.Token);
}
private void Process(BackupStateJob job, CancellationToken ct)
{
ProcessAsync(job, ct).Forget();
}
private async Task ProcessAsync(BackupStateJob job, CancellationToken ct)
{
var jobId = job.Id.ToString();
var handlers = CreateHandlers();
var lastTimestamp = job.Started;
try
{
using (var stream = await backupArchiveLocation.OpenStreamAsync(job.Id))
using (var stream = await backupArchiveLocation.OpenStreamAsync(jobId))
{
using (var writer = new BackupWriter(serializer, stream, true))
{
@ -147,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
job.HandledAssets = writer.WrittenAttachments;
lastTimestamp = await WritePeriodically(lastTimestamp);
}, SquidexHeaders.AppId, Key.ToString(), null, currentTask.Token);
}, SquidexHeaders.AppId, Key.ToString(), null, ct);
foreach (var handler in handlers)
{
@ -162,16 +179,16 @@ namespace Squidex.Domain.Apps.Entities.Backup
stream.Position = 0;
currentTask.Token.ThrowIfCancellationRequested();
ct.ThrowIfCancellationRequested();
await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream, false, currentTask.Token);
await assetStore.UploadAsync(jobId, 0, null, stream, false, ct);
}
job.Status = JobStatus.Completed;
}
catch (Exception ex)
{
log.LogError(ex, job.Id.ToString(), (ctx, w) => w
log.LogError(ex, jobId, (ctx, w) => w
.WriteProperty("action", "makeBackup")
.WriteProperty("status", "failed")
.WriteProperty("backupId", ctx));
@ -180,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
}
finally
{
await Safe.DeleteAsync(backupArchiveLocation, job.Id, log);
await Safe.DeleteAsync(backupArchiveLocation, jobId, log);
job.Stopped = clock.GetCurrentInstant();
@ -220,8 +237,10 @@ namespace Squidex.Domain.Apps.Entities.Backup
}
else
{
await Safe.DeleteAsync(backupArchiveLocation, job.Id, log);
await Safe.DeleteAsync(assetStore, job.Id, log);
var jobId = job.Id.ToString();
await Safe.DeleteAsync(backupArchiveLocation, jobId, log);
await Safe.DeleteAsync(assetStore, jobId, log);
State.Jobs.Remove(job);
@ -229,6 +248,11 @@ namespace Squidex.Domain.Apps.Entities.Backup
}
}
private IEnumerable<BackupHandler> CreateHandlers()
{
return serviceProvider.GetRequiredService<IEnumerable<BackupHandler>>();
}
public Task<J<List<IBackupJob>>> GetStateAsync()
{
return J.AsTask(State.Jobs.OfType<IBackupJob>().ToList());

2
src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs

@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
return TaskHelper.Done;
}
public virtual Task CleanupRestoreAsync(Guid appId)
public virtual Task CleanupRestoreErrorAsync(Guid appId)
{
return TaskHelper.Done;
}

4
src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Backup.Helpers
{
public static class Downloader
{
public static async Task DownloadAsync(this IBackupArchiveLocation backupArchiveLocation, Uri url, Guid id)
public static async Task DownloadAsync(this IBackupArchiveLocation backupArchiveLocation, Uri url, string id)
{
if (string.Equals(url.Scheme, "file"))
{
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Backup.Helpers
}
}
public static async Task<BackupReader> OpenArchiveAsync(this IBackupArchiveLocation backupArchiveLocation, Guid id, IJsonSerializer serializer)
public static async Task<BackupReader> OpenArchiveAsync(this IBackupArchiveLocation backupArchiveLocation, string id, IJsonSerializer serializer)
{
Stream stream = null;

14
src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Backup.Helpers
{
public static class Safe
{
public static async Task DeleteAsync(IBackupArchiveLocation backupArchiveLocation, Guid id, ISemanticLog log)
public static async Task DeleteAsync(IBackupArchiveLocation backupArchiveLocation, string id, ISemanticLog log)
{
try
{
@ -22,33 +22,33 @@ namespace Squidex.Domain.Apps.Entities.Backup.Helpers
}
catch (Exception ex)
{
log.LogError(ex, id.ToString(), (logOperationId, w) => w
log.LogError(ex, id, (logOperationId, w) => w
.WriteProperty("action", "deleteArchive")
.WriteProperty("status", "failed")
.WriteProperty("operationId", logOperationId));
}
}
public static async Task DeleteAsync(IAssetStore assetStore, Guid id, ISemanticLog log)
public static async Task DeleteAsync(IAssetStore assetStore, string id, ISemanticLog log)
{
try
{
await assetStore.DeleteAsync(id.ToString(), 0, null);
await assetStore.DeleteAsync(id, 0, null);
}
catch (Exception ex)
{
log.LogError(ex, id.ToString(), (logOperationId, w) => w
log.LogError(ex, id, (logOperationId, w) => w
.WriteProperty("action", "deleteBackup")
.WriteProperty("status", "failed")
.WriteProperty("operationId", logOperationId));
}
}
public static async Task CleanupRestoreAsync(BackupHandler handler, Guid appId, Guid id, ISemanticLog log)
public static async Task CleanupRestoreErrorAsync(BackupHandler handler, Guid appId, Guid id, ISemanticLog log)
{
try
{
await handler.CleanupRestoreAsync(appId);
await handler.CleanupRestoreErrorAsync(appId);
}
catch (Exception ex)
{

5
src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
@ -13,8 +12,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
public interface IBackupArchiveLocation
{
Task<Stream> OpenStreamAsync(Guid backupId);
Task<Stream> OpenStreamAsync(string backupId);
Task DeleteArchiveAsync(Guid backupId);
Task DeleteArchiveAsync(string backupId);
}
}

46
src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -31,11 +32,11 @@ namespace Squidex.Domain.Apps.Entities.Backup
private readonly IBackupArchiveLocation backupArchiveLocation;
private readonly IClock clock;
private readonly ICommandBus commandBus;
private readonly IEnumerable<BackupHandler> handlers;
private readonly IJsonSerializer serializer;
private readonly IEventStore eventStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly ISemanticLog log;
private readonly IServiceProvider serviceProvider;
private readonly IStreamNameResolver streamNameResolver;
private RestoreStateJob CurrentJob
@ -48,9 +49,9 @@ namespace Squidex.Domain.Apps.Entities.Backup
ICommandBus commandBus,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
IEnumerable<BackupHandler> handlers,
IJsonSerializer serializer,
ISemanticLog log,
IServiceProvider serviceProvider,
IStreamNameResolver streamNameResolver,
IStore<string> store)
: base(store)
@ -60,8 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(handlers, nameof(handlers));
Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(store, nameof(store));
Guard.NotNull(streamNameResolver, nameof(streamNameResolver));
Guard.NotNull(log, nameof(log));
@ -71,8 +72,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
this.commandBus = commandBus;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.handlers = handlers;
this.serializer = serializer;
this.serviceProvider = serviceProvider;
this.streamNameResolver = streamNameResolver;
this.log = log;
}
@ -88,16 +89,18 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
if (CurrentJob?.Status == JobStatus.Started)
{
var handlers = CreateHandlers();
Log("Failed due application restart");
CurrentJob.Status = JobStatus.Failed;
await CleanupAsync();
await CleanupAsync(handlers);
await WriteStateAsync();
}
}
public Task RestoreAsync(Uri url, RefToken actor, string newAppName)
public async Task RestoreAsync(Uri url, RefToken actor, string newAppName)
{
Guard.NotNull(url, nameof(url));
Guard.NotNull(actor, nameof(actor));
@ -122,9 +125,9 @@ namespace Squidex.Domain.Apps.Entities.Backup
Url = url
};
Process();
await WriteStateAsync();
return TaskHelper.Done;
Process();
}
private void Process()
@ -134,6 +137,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
private async Task ProcessAsync()
{
var handlers = CreateHandlers();
var logContext = (jobId: CurrentJob.Id.ToString(), jobUrl: CurrentJob.Url.ToString());
using (Profiler.StartSession())
@ -157,11 +162,11 @@ namespace Squidex.Domain.Apps.Entities.Backup
await DownloadAsync();
}
using (var reader = await backupArchiveLocation.OpenArchiveAsync(CurrentJob.Id, serializer))
using (var reader = await backupArchiveLocation.OpenArchiveAsync(CurrentJob.Id.ToString(), serializer))
{
using (Profiler.Trace("ReadEvents"))
{
await ReadEventsAsync(reader);
await ReadEventsAsync(reader, handlers);
}
foreach (var handler in handlers)
@ -212,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
Log("Failed with internal error");
}
await CleanupAsync();
await CleanupAsync(handlers);
CurrentJob.Status = JobStatus.Failed;
@ -258,15 +263,15 @@ namespace Squidex.Domain.Apps.Entities.Backup
}
}
private async Task CleanupAsync()
private async Task CleanupAsync(IEnumerable<BackupHandler> handlers)
{
await Safe.DeleteAsync(backupArchiveLocation, CurrentJob.Id, log);
await Safe.DeleteAsync(backupArchiveLocation, CurrentJob.Id.ToString(), log);
if (CurrentJob.AppId != Guid.Empty)
{
foreach (var handler in handlers)
{
await Safe.CleanupRestoreAsync(handler, CurrentJob.AppId, CurrentJob.Id, log);
await Safe.CleanupRestoreErrorAsync(handler, CurrentJob.AppId, CurrentJob.Id, log);
}
}
}
@ -275,22 +280,22 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
Log("Downloading Backup");
await backupArchiveLocation.DownloadAsync(CurrentJob.Url, CurrentJob.Id);
await backupArchiveLocation.DownloadAsync(CurrentJob.Url, CurrentJob.Id.ToString());
Log("Downloaded Backup");
}
private async Task ReadEventsAsync(BackupReader reader)
private async Task ReadEventsAsync(BackupReader reader, IEnumerable<BackupHandler> handlers)
{
await reader.ReadEventsAsync(streamNameResolver, eventDataFormatter, async storedEvent =>
{
await HandleEventAsync(reader, storedEvent.Stream, storedEvent.Event);
await HandleEventAsync(reader, handlers, storedEvent.Stream, storedEvent.Event);
});
Log($"Reading {reader.ReadEvents} events and {reader.ReadAttachments} attachments completed.", true);
}
private async Task HandleEventAsync(BackupReader reader, string stream, Envelope<IEvent> @event)
private async Task HandleEventAsync(BackupReader reader, IEnumerable<BackupHandler> handlers, string stream, Envelope<IEvent> @event)
{
if (@event.Payload is SquidexEvent squidexEvent)
{
@ -340,6 +345,11 @@ namespace Squidex.Domain.Apps.Entities.Backup
}
}
private IEnumerable<BackupHandler> CreateHandlers()
{
return serviceProvider.GetRequiredService<IEnumerable<BackupHandler>>();
}
public Task<J<IRestoreJob>> GetJobAsync()
{
return Task.FromResult<J<IRestoreJob>>(CurrentJob);

7
src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
@ -14,14 +13,14 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation
{
public Task<Stream> OpenStreamAsync(Guid backupId)
public Task<Stream> OpenStreamAsync(string backupId)
{
var tempFile = GetTempFile(backupId);
return Task.FromResult<Stream>(new FileStream(tempFile, FileMode.OpenOrCreate, FileAccess.ReadWrite));
}
public Task DeleteArchiveAsync(Guid backupId)
public Task DeleteArchiveAsync(string backupId)
{
var tempFile = GetTempFile(backupId);
@ -36,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
return TaskHelper.Done;
}
private static string GetTempFile(Guid backupId)
private static string GetTempFile(string backupId)
{
return Path.Combine(Path.GetTempPath(), backupId + ".zip");
}

11
src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs

@ -20,6 +20,8 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
public ConverterContractResolver(params JsonConverter[] converters)
{
NamingStrategy = new CamelCaseNamingStrategy(false, true);
this.converters = converters;
foreach (var converter in converters)
@ -34,15 +36,6 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
}
}
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
contract.DictionaryKeyResolver = propertyName => propertyName;
return contract;
}
protected override JsonConverter ResolveContractConverter(Type objectType)
{
var result = base.ResolveContractConverter(objectType);

708
src/Squidex.Infrastructure/StringExtensions.cs

@ -24,287 +24,499 @@ namespace Squidex.Infrastructure
private static readonly Dictionary<char, string> LowerCaseDiacritics;
private static readonly Dictionary<char, string> Diacritics = new Dictionary<char, string>
{
['$'] = "dollar",
['%'] = "percent",
['&'] = "and",
['<'] = "less",
['>'] = "greater",
['|'] = "or",
['¢'] = "cent",
['£'] = "pound",
['¤'] = "currency",
['¥'] = "yen",
['©'] = "(c)",
['ª'] = "a",
['®'] = "(r)",
['º'] = "o",
['À'] = "A",
['à'] = "a",
['Ā'] = "A",
['Ġ'] = "G",
['ŀ'] = "l",
['Š'] = "S",
['Ǡ'] = "A",
['Ȁ'] = "A",
['Á'] = "A",
['á'] = "a",
['ā'] = "a",
['ġ'] = "g",
['Ł'] = "L",
['š'] = "s",
['ǡ'] = "a",
['ȁ'] = "a",
['Â'] = "A",
['â'] = "a",
['Ă'] = "A",
['Ģ'] = "G",
['ł'] = "l",
['Ţ'] = "T",
['Ǣ'] = "Ae",
['Ȃ'] = "A",
['Ã'] = "A",
['ã'] = "a",
['ă'] = "a",
['ģ'] = "g",
['Ń'] = "N",
['ţ'] = "t",
['ǣ'] = "ae",
['ȃ'] = "a",
['Ä'] = "Ae",
['ä'] = "ae",
['Ą'] = "A",
['Ĥ'] = "H",
['ń'] = "n",
['Ť'] = "T",
['DŽ'] = "DZ",
['Ǥ'] = "G",
['Ȅ'] = "E",
['Ä'] = "AE",
['Å'] = "A",
['å'] = "a",
['ą'] = "a",
['ĥ'] = "h",
['Ņ'] = "N",
['ť'] = "t",
['Dž'] = "Dz",
['ǥ'] = "g",
['ȅ'] = "e",
['Æ'] = "AE",
['æ'] = "ae",
['Ć'] = "C",
['Ħ'] = "H",
['ņ'] = "n",
['Ŧ'] = "T",
['dž'] = "dz",
['Ǧ'] = "G",
['Ȇ'] = "E",
['Ç'] = "C",
['ç'] = "c",
['ć'] = "c",
['ħ'] = "h",
['Ň'] = "N",
['ŧ'] = "t",
['LJ'] = "W",
['ǧ'] = "g",
['ȇ'] = "e",
['È'] = "E",
['è'] = "E",
['Ĉ'] = "C",
['Ĩ'] = "I",
['ň'] = "n",
['Ũ'] = "U",
['Lj'] = "Lj",
['Ǩ'] = "K",
['Ȉ'] = "I",
['É'] = "E",
['é'] = "e",
['ĉ'] = "c",
['ĩ'] = "i",
['ʼn'] = "n",
['ũ'] = "u",
['lj'] = "lj",
['ǩ'] = "k",
['ȉ'] = "i",
['Ê'] = "E",
['ê'] = "e",
['Ċ'] = "C",
['Ī'] = "I",
['Ŋ'] = "n",
['Ū'] = "U",
['NJ'] = "NJ",
['Ǫ'] = "O",
['Ȋ'] = "I",
['Ë'] = "E",
['ë'] = "e",
['ċ'] = "c",
['ī'] = "i",
['ŋ'] = "n",
['ū'] = "u",
['Nj'] = "Nj",
['ǫ'] = "o",
['ȋ'] = "i",
['Ì'] = "I",
['ì'] = "i",
['Č'] = "C",
['Ĭ'] = "I",
['Ō'] = "O",
['Ŭ'] = "U",
['nj'] = "nj",
['Ǭ'] = "O",
['Ȍ'] = "O",
['Í'] = "I",
['í'] = "i",
['č'] = "c",
['ĭ'] = "i",
['ō'] = "o",
['ŭ'] = "u",
['Ǎ'] = "A",
['ǭ'] = "o",
['ȍ'] = "o",
['Î'] = "I",
['î'] = "i",
['Ď'] = "D",
['Į'] = "I",
['Ŏ'] = "O",
['Ů'] = "U",
['ǎ'] = "a",
['Ǯ'] = "z",
['Ȏ'] = "O",
['Ï'] = "I",
['ï'] = "i",
['ď'] = "d",
['į'] = "i",
['ŏ'] = "o",
['ů'] = "u",
['Ǐ'] = "I",
['ǯ'] = "z",
['ȏ'] = "o",
['Ð'] = "D",
['ð'] = "d",
['Đ'] = "D",
['İ'] = "I",
['Ő'] = "O",
['Ű'] = "U",
['ǐ'] = "i",
['ǰ'] = "j",
['Ȑ'] = "R",
['Ñ'] = "N",
['ñ'] = "n",
['đ'] = "d",
['ı'] = "i",
['ő'] = "o",
['ű'] = "u",
['Ǒ'] = "O",
['DZ'] = "DZ",
['ȑ'] = "r",
['Ò'] = "O",
['ò'] = "o",
['Ē'] = "E",
['IJ'] = "LJ",
['Œ'] = "OE",
['Ų'] = "U",
['ǒ'] = "o",
['Dz'] = "Dz",
['Ȓ'] = "R",
['Ó'] = "O",
['ó'] = "o",
['ē'] = "e",
['ij'] = "ij",
['œ'] = "oe",
['ų'] = "u",
['Ǔ'] = "U",
['dz'] = "dz",
['ȓ'] = "r",
['Ô'] = "O",
['ô'] = "o",
['Ĕ'] = "E",
['Ĵ'] = "J",
['Ŕ'] = "R",
['Ŵ'] = "W",
['ǔ'] = "u",
['Ǵ'] = "G",
['Ȕ'] = "U",
['Õ'] = "O",
['Ö'] = "OE",
['Ø'] = "O",
['Ù'] = "U",
['Ú'] = "U",
['Û'] = "U",
['Ü'] = "UE",
['Ý'] = "Y",
['Þ'] = "TH",
['ß'] = "ss",
['à'] = "a",
['á'] = "a",
['â'] = "a",
['ã'] = "a",
['ä'] = "ae",
['å'] = "a",
['æ'] = "ae",
['ç'] = "c",
['è'] = "e",
['é'] = "e",
['ê'] = "e",
['ë'] = "e",
['ì'] = "i",
['í'] = "i",
['î'] = "i",
['ï'] = "i",
['ð'] = "d",
['ñ'] = "n",
['ò'] = "o",
['ó'] = "o",
['ô'] = "o",
['õ'] = "o",
['ĕ'] = "e",
['ĵ'] = "j",
['ŕ'] = "r",
['ŵ'] = "w",
['Ǖ'] = "U",
['ǵ'] = "g",
['ȕ'] = "u",
['Ö'] = "Oe",
['ö'] = "oe",
['Ė'] = "E",
['Ķ'] = "K",
['Ŗ'] = "R",
['Ŷ'] = "Y",
['ǖ'] = "u",
['Ƕ'] = "Hj",
['Ȗ'] = "U",
['ė'] = "e",
['ķ'] = "k",
['ŗ'] = "r",
['ŷ'] = "y",
['Ǘ'] = "U",
['ȗ'] = "u",
['Ø'] = "O",
['ø'] = "o",
['Ę'] = "E",
['ĸ'] = "k",
['Ř'] = "R",
['Ÿ'] = "Y",
['ǘ'] = "u",
['Ǹ'] = "N",
['Ș'] = "S",
['Ù'] = "U",
['ù'] = "u",
['ę'] = "e",
['Ĺ'] = "L",
['ř'] = "r",
['Ź'] = "Z",
['Ǚ'] = "U",
['ǹ'] = "n",
['ș'] = "s",
['Ú'] = "U",
['ú'] = "u",
['Ě'] = "E",
['ĺ'] = "l",
['Ś'] = "S",
['ź'] = "z",
['ǚ'] = "u",
['Ǻ'] = "A",
['Ț'] = "T",
['Û'] = "U",
['û'] = "u",
['ü'] = "ue",
['ý'] = "y",
['þ'] = "th",
['ÿ'] = "y",
['Ā'] = "A",
['ā'] = "a",
['Ă'] = "A",
['ă'] = "a",
['Ą'] = "A",
['ą'] = "a",
['Ć'] = "C",
['ć'] = "c",
['Č'] = "C",
['č'] = "c",
['Ď'] = "D",
['ď'] = "d",
['Đ'] = "DJ",
['đ'] = "dj",
['Ē'] = "E",
['ē'] = "e",
['Ė'] = "E",
['ė'] = "e",
['Ę'] = "e",
['ę'] = "e",
['Ě'] = "E",
['ě'] = "e",
['Ğ'] = "G",
['ğ'] = "g",
['Ģ'] = "G",
['ģ'] = "g",
['Ĩ'] = "I",
['ĩ'] = "i",
['Ī'] = "i",
['ī'] = "i",
['Į'] = "I",
['į'] = "i",
['İ'] = "I",
['ı'] = "i",
['Ķ'] = "k",
['ķ'] = "k",
['Ļ'] = "L",
['ļ'] = "l",
['Ľ'] = "L",
['ľ'] = "l",
['Ł'] = "L",
['ł'] = "l",
['Ń'] = "N",
['ń'] = "n",
['Ņ'] = "N",
['ņ'] = "n",
['Ň'] = "N",
['ň'] = "n",
['Ő'] = "O",
['ő'] = "o",
['Œ'] = "OE",
['œ'] = "oe",
['Ŕ'] = "R",
['ŕ'] = "r",
['Ř'] = "R",
['ř'] = "r",
['Ś'] = "S",
['ś'] = "s",
['Ş'] = "S",
['ş'] = "s",
['Š'] = "S",
['š'] = "s",
['Ţ'] = "T",
['ţ'] = "t",
['Ť'] = "T",
['ť'] = "t",
['Ũ'] = "U",
['ũ'] = "u",
['Ū'] = "u",
['ū'] = "u",
['Ů'] = "U",
['ů'] = "u",
['Ű'] = "U",
['ű'] = "u",
['Ų'] = "U",
['ų'] = "u",
['Ź'] = "Z",
['ź'] = "z",
['Ż'] = "Z",
['Ǜ'] = "U",
['ǻ'] = "a",
['ț'] = "t",
['Ü'] = "Ue",
['ü'] = "ue",
['Ĝ'] = "G",
['ļ'] = "l",
['Ŝ'] = "S",
['ż'] = "z",
['ǜ'] = "u",
['Ǽ'] = "AE",
['Ȝ'] = "z",
['Ý'] = "Y",
['ý'] = "y",
['ĝ'] = "g",
['Ľ'] = "L",
['ŝ'] = "s",
['Ž'] = "Z",
['ǝ'] = "e",
['ǽ'] = "ae",
['ȝ'] = "z",
['Þ'] = "p",
['þ'] = "p",
['Ğ'] = "G",
['ľ'] = "L",
['Ş'] = "S",
['ž'] = "z",
['Ǟ'] = "A",
['Ǿ'] = "O",
['Ȟ'] = "H",
['ß'] = "ss",
['ÿ'] = "y",
['ğ'] = "g",
['Ŀ'] = "L",
['ş'] = "s",
['ſ'] = "l",
['ǟ'] = "a",
['ǿ'] = "o",
['ȟ'] = "h"
['ƒ'] = "f",
['Ơ'] = "O",
['ơ'] = "o",
['Ư'] = "U",
['ư'] = "u",
['Lj'] = "LJ",
['lj'] = "lj",
['Nj'] = "NJ",
['nj'] = "nj",
['Ș'] = "S",
['ș'] = "s",
['Ț'] = "T",
['ț'] = "t",
['˚'] = "o",
['Ά'] = "A",
['Έ'] = "E",
['Ή'] = "H",
['Ί'] = "I",
['Ό'] = "O",
['Ύ'] = "Y",
['Ώ'] = "W",
['ΐ'] = "i",
['Α'] = "A",
['Β'] = "B",
['Γ'] = "G",
['Δ'] = "D",
['Ε'] = "E",
['Ζ'] = "Z",
['Η'] = "H",
['Θ'] = "8",
['Ι'] = "I",
['Κ'] = "K",
['Λ'] = "L",
['Μ'] = "M",
['Ν'] = "N",
['Ξ'] = "3",
['Ο'] = "O",
['Π'] = "P",
['Ρ'] = "R",
['Σ'] = "S",
['Τ'] = "T",
['Υ'] = "Y",
['Φ'] = "F",
['Χ'] = "X",
['Ψ'] = "PS",
['Ω'] = "W",
['Ϊ'] = "I",
['Ϋ'] = "Y",
['ά'] = "a",
['έ'] = "e",
['ή'] = "h",
['ί'] = "i",
['ΰ'] = "y",
['α'] = "a",
['β'] = "b",
['γ'] = "g",
['δ'] = "d",
['ε'] = "e",
['ζ'] = "z",
['η'] = "h",
['θ'] = "8",
['ι'] = "i",
['κ'] = "k",
['λ'] = "l",
['μ'] = "m",
['ν'] = "n",
['ξ'] = "3",
['ο'] = "o",
['π'] = "p",
['ρ'] = "r",
['ς'] = "s",
['σ'] = "s",
['τ'] = "t",
['υ'] = "y",
['φ'] = "f",
['χ'] = "x",
['ψ'] = "ps",
['ω'] = "w",
['ϊ'] = "i",
['ϋ'] = "y",
['ό'] = "o",
['ύ'] = "y",
['ώ'] = "w",
['Ё'] = "Yo",
['Ђ'] = "DJ",
['Є'] = "Ye",
['І'] = "I",
['Ї'] = "Yi",
['Ј'] = "J",
['Љ'] = "LJ",
['Њ'] = "NJ",
['Ћ'] = "C",
['Џ'] = "DZ",
['А'] = "A",
['Б'] = "B",
['В'] = "V",
['Г'] = "G",
['Д'] = "D",
['Е'] = "E",
['Ж'] = "Zh",
['З'] = "Z",
['И'] = "I",
['Й'] = "J",
['К'] = "K",
['Л'] = "L",
['М'] = "M",
['Н'] = "N",
['О'] = "O",
['П'] = "P",
['Р'] = "R",
['С'] = "S",
['Т'] = "T",
['У'] = "U",
['Ф'] = "F",
['Х'] = "H",
['Ц'] = "C",
['Ч'] = "Ch",
['Ш'] = "Sh",
['Щ'] = "Sh",
['Ъ'] = "U",
['Ы'] = "Y",
['Ь'] = "b",
['Э'] = "E",
['Ю'] = "Yu",
['Я'] = "Ya",
['а'] = "a",
['б'] = "b",
['в'] = "v",
['г'] = "g",
['д'] = "d",
['е'] = "e",
['ж'] = "zh",
['з'] = "z",
['и'] = "i",
['й'] = "j",
['к'] = "k",
['л'] = "l",
['м'] = "m",
['н'] = "n",
['о'] = "o",
['п'] = "p",
['р'] = "r",
['с'] = "s",
['т'] = "t",
['у'] = "u",
['ф'] = "f",
['х'] = "h",
['ц'] = "c",
['ч'] = "ch",
['ш'] = "sh",
['щ'] = "sh",
['ъ'] = "u",
['ы'] = "y",
['ь'] = "s",
['э'] = "e",
['ю'] = "yu",
['я'] = "ya",
['ё'] = "yo",
['ђ'] = "dj",
['є'] = "ye",
['і'] = "i",
['ї'] = "yi",
['ј'] = "j",
['љ'] = "lj",
['њ'] = "nj",
['ћ'] = "c",
['џ'] = "dz",
['Ґ'] = "G",
['ґ'] = "g",
['฿'] = "baht",
['ა'] = "a",
['ბ'] = "b",
['გ'] = "g",
['დ'] = "d",
['ე'] = "e",
['ვ'] = "v",
['ზ'] = "z",
['თ'] = "t",
['ი'] = "i",
['კ'] = "k",
['ლ'] = "l",
['მ'] = "m",
['ნ'] = "n",
['ო'] = "o",
['პ'] = "p",
['ჟ'] = "zh",
['რ'] = "r",
['ს'] = "s",
['ტ'] = "t",
['უ'] = "u",
['ფ'] = "f",
['ქ'] = "k",
['ღ'] = "gh",
['ყ'] = "q",
['შ'] = "sh",
['ჩ'] = "ch",
['ც'] = "ts",
['ძ'] = "dz",
['წ'] = "ts",
['ჭ'] = "ch",
['ხ'] = "kh",
['ჯ'] = "j",
['ჰ'] = "h",
['ẞ'] = "SS",
['Ạ'] = "A",
['ạ'] = "a",
['Ả'] = "A",
['ả'] = "a",
['Ấ'] = "A",
['ấ'] = "a",
['Ầ'] = "A",
['ầ'] = "a",
['Ẩ'] = "A",
['ẩ'] = "a",
['Ẫ'] = "A",
['ẫ'] = "a",
['Ậ'] = "A",
['ậ'] = "a",
['Ắ'] = "A",
['ắ'] = "a",
['Ằ'] = "A",
['ằ'] = "a",
['Ẳ'] = "A",
['ẳ'] = "a",
['Ẵ'] = "A",
['ẵ'] = "a",
['Ặ'] = "A",
['ặ'] = "a",
['Ẹ'] = "E",
['ẹ'] = "e",
['Ẻ'] = "E",
['ẻ'] = "e",
['Ẽ'] = "E",
['ẽ'] = "e",
['Ế'] = "E",
['ế'] = "e",
['Ề'] = "E",
['ề'] = "e",
['Ể'] = "E",
['ể'] = "e",
['Ễ'] = "E",
['ễ'] = "e",
['Ệ'] = "E",
['ệ'] = "e",
['Ỉ'] = "I",
['ỉ'] = "i",
['Ị'] = "I",
['ị'] = "i",
['Ọ'] = "O",
['ọ'] = "o",
['Ỏ'] = "O",
['ỏ'] = "o",
['Ố'] = "O",
['ố'] = "o",
['Ồ'] = "O",
['ồ'] = "o",
['Ổ'] = "O",
['ổ'] = "o",
['Ỗ'] = "O",
['ỗ'] = "o",
['Ộ'] = "O",
['ộ'] = "o",
['Ớ'] = "O",
['ớ'] = "o",
['Ờ'] = "O",
['ờ'] = "o",
['Ở'] = "O",
['ở'] = "o",
['Ỡ'] = "O",
['ỡ'] = "o",
['Ợ'] = "O",
['ợ'] = "o",
['Ụ'] = "U",
['ụ'] = "u",
['Ủ'] = "U",
['ủ'] = "u",
['Ứ'] = "U",
['ứ'] = "u",
['Ừ'] = "U",
['ừ'] = "u",
['Ử'] = "U",
['ử'] = "u",
['Ữ'] = "U",
['ữ'] = "u",
['Ự'] = "U",
['ự'] = "u",
['Ỳ'] = "Y",
['ỳ'] = "y",
['Ỵ'] = "Y",
['ỵ'] = "y",
['Ỷ'] = "Y",
['ỷ'] = "y",
['Ỹ'] = "Y",
['ỹ'] = "y",
['‘'] = "\'",
['’'] = "\'",
['“'] = "\\\"",
['”'] = "\\\"",
['†'] = "+",
['•'] = "*",
['…'] = "...",
['₠'] = "ecu",
['₢'] = "cruzeiro",
['₣'] = "french franc",
['₤'] = "lira",
['₥'] = "mill",
['₦'] = "naira",
['₧'] = "peseta",
['₨'] = "rupee",
['₩'] = "won",
['₪'] = "new shequel",
['₫'] = "dong",
['€'] = "euro",
['₭'] = "kip",
['₮'] = "tugrik",
['₯'] = "drachma",
['₰'] = "penny",
['₱'] = "peso",
['₲'] = "guarani",
['₳'] = "austral",
['₴'] = "hryvnia",
['₵'] = "cedi",
['₹'] = "indian rupee",
['₽'] = "russian ruble",
['₿'] = "bitcoin",
['℠'] = "sm",
['™'] = "tm",
['∂'] = "d",
['∆'] = "delta",
['∑'] = "sum",
['∞'] = "infinity",
['♥'] = "love",
['元'] = "yuan",
['円'] = "yen",
['﷼'] = "rial"
};
static StringExtensions()
@ -530,7 +742,7 @@ namespace Squidex.Infrastructure
if (LowerCaseDiacritics.TryGetValue(character, out var replacement))
{
if (singleCharDiactric)
if (singleCharDiactric && replacement.Length == 2)
{
result.Append(replacement[0]);
}

23
src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs

@ -6,8 +6,11 @@
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Web;
@ -21,11 +24,13 @@ namespace Squidex.Areas.Api.Controllers.Backups
public class BackupContentController : ApiController
{
private readonly IAssetStore assetStore;
private readonly IGrainFactory grainFactory;
public BackupContentController(ICommandBus commandBus, IAssetStore assetStore)
public BackupContentController(ICommandBus commandBus, IAssetStore assetStore, IGrainFactory grainFactory)
: base(commandBus)
{
this.assetStore = assetStore;
this.grainFactory = grainFactory;
}
/// <summary>
@ -43,9 +48,21 @@ namespace Squidex.Areas.Api.Controllers.Backups
[ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0)]
[AllowAnonymous]
public IActionResult GetBackupContent(string app, Guid id)
public async Task<IActionResult> GetBackupContent(string app, Guid id)
{
return new FileCallbackResult("application/zip", "Backup.zip", false, bodyStream =>
var backupGrain = grainFactory.GetGrain<IBackupGrain>(AppId);
var backups = await backupGrain.GetStateAsync();
var backup = backups.Value.Find(x => x.Id == id);
if (backup == null || backup.Status != JobStatus.Completed)
{
return NotFound();
}
var fileName = $"backup-{app}-{backup.Started:yyyy-MM-dd_HH-mm-ss}.zip";
return new FileCallbackResult("application/zip", fileName, false, bodyStream =>
{
return assetStore.DownloadAsync(id.ToString(), 0, null, bodyStream);
});

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs

@ -73,6 +73,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public string[] AllowedExtensions { get; set; }
/// <summary>
/// True, if duplicate values are allowed.
/// </summary>
public bool AllowDuplicates { get; set; }
public override FieldProperties ToProperties()
{
var result = SimpleMapper.Map(this, new AssetsFieldProperties());

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs

@ -23,6 +23,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public int? MaxItems { get; set; }
/// <summary>
/// True, if duplicate values are allowed.
/// </summary>
public bool AllowDuplicates { get; set; }
/// <summary>
/// The id of the referenced schema.
/// </summary>

11
src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html

@ -94,6 +94,17 @@
<label class="col-form-label">px</label>
</div>
</div>
<div class="form-group row">
<div class="col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldAllowDuplicates" formControlName="allowDuplicates" />
<label class="form-check-label" for="{{field.fieldId}}_fieldAllowDuplicates">
Allow duplicate values
</label>
</div>
</div>
</div>
<div class="form-group2 row">
<label class="col-3 col-form-label">

3
src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts

@ -61,5 +61,8 @@ export class AssetsValidationComponent implements OnInit {
this.editForm.setControl('aspectHeight',
new FormControl(this.properties.aspectHeight));
this.editForm.setControl('allowDuplicates',
new FormControl(this.properties.allowDuplicates));
}
}

11
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html

@ -21,6 +21,17 @@
</div>
</div>
<div class="form-group row">
<div class="col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldAllowDuplicates" formControlName="allowDuplicates" />
<label class="form-check-label" for="{{field.fieldId}}_fieldAllowDuplicates">
Allow duplicate values
</label>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">Items</label>

3
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts

@ -31,6 +31,9 @@ export class ReferencesValidationComponent implements OnInit {
}
public ngOnInit() {
this.editForm.setControl('allowDuplicates',
new FormControl(this.properties.allowDuplicates));
this.editForm.setControl('maxItems',
new FormControl(this.properties.maxItems));

4
src/Squidex/app/framework/angular/forms/checkbox-group.component.html

@ -1,4 +1,4 @@
<span class="form-check" *ngFor="let value of values">
<div class="form-check" *ngFor="let value of values">
<input type="checkbox" class="form-check-input" id="{{controlId}}{{value}}"
(blur)="callTouched()"
(change)="check($event.target.checked, value)"
@ -6,4 +6,4 @@
[disabled]="snapshot.isDisabled">
<label class="form-check-label" for="{{controlId}}{{value}}">{{value}}</label>
</span>
</div>

8
src/Squidex/app/framework/angular/forms/checkbox-group.component.scss

@ -2,11 +2,15 @@
@import '_vars';
.form-check {
display: inline-block;
display: block;
margin-left: 0;
margin-right: 1rem;
margin-bottom: .5rem;
}
.form-check-input {
margin-top: .4rem;
}
label {
min-width: 5rem;
}

13
src/Squidex/app/framework/angular/forms/control-errors.component.ts

@ -61,9 +61,7 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
public ngOnDestroy() {
super.ngOnDestroy();
if (this.control && this.originalMarkAsTouched) {
this.control['markAsTouched'] = this.originalMarkAsTouched;
}
this.unsetCustomMarkAsTouchedFunction();
}
public ngOnChanges() {
@ -87,6 +85,7 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
if (this.control !== control) {
this.unsubscribeAll();
this.unsetCustomMarkAsTouchedFunction();
this.control = control;
@ -112,6 +111,12 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
this.createMessages();
}
private unsetCustomMarkAsTouchedFunction() {
if (this.control && this.originalMarkAsTouched) {
this.control['markAsTouched'] = this.originalMarkAsTouched;
}
}
private createMessages() {
const errors: string[] = [];
@ -131,4 +136,4 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
this.next(s => ({ ...s, errorMessages: errors }));
}
}
}
}

6
src/Squidex/app/framework/angular/forms/error-formatting.spec.ts

@ -132,6 +132,12 @@ describe('formatErrors', () => {
expect(error).toEqual('MY_FIELD contains an invalid value: 4.');
});
it('should format uniqueStrings', () => {
const error = validate(['1', '2', '2', '3'], ValidatorsEx.uniqueStrings());
expect(error).toEqual('MY_FIELD must not contain duplicate values.');
});
it('should format match', () => {
const formControl1 = new FormControl(1);
const formControl2 = new FormControl(2);

1
src/Squidex/app/framework/angular/forms/error-formatting.ts

@ -26,6 +26,7 @@ const DEFAULT_ERRORS: { [key: string]: string } = {
patternmessage: '{message}',
required: '{field} is required.',
requiredTrue: '{field} is required.',
uniquestrings: '{field} must not contain duplicate values.',
validdatetime: '{field} is not a valid date time.',
validvalues: '{field} is not a valid value.',
validarrayvalues: '{field} contains an invalid value: {invalidvalue}.'

34
src/Squidex/app/framework/angular/forms/validators.spec.ts

@ -371,4 +371,38 @@ describe('ValidatorsEx.pattern', () => {
expect(error).toEqual(expected);
});
});
describe('ValidatorsEx.uniqueStrings', () => {
it('should return null when value is null', () => {
const input = new FormControl(null);
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toBeNull();
});
it('should return null when value is not a string array', () => {
const input = new FormControl([1, 2, 3]);
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toBeNull();
});
it('should return null when values are unique', () => {
const input = new FormControl(['1', '2', '3']);
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toBeNull();
});
it('should return error when values are not unique', () => {
const input = new FormControl(['1', '2', '2', '3']);
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toEqual({ uniquestrings: false });
});
});

23
src/Squidex/app/framework/angular/forms/validators.ts

@ -7,7 +7,7 @@
import { AbstractControl, ValidatorFn, Validators } from '@angular/forms';
import { DateTime } from '@app/framework/internal';
import { DateTime, Types } from '@app/framework/internal';
function isEmptyInputValue(value: any): boolean {
return value == null || value.length === 0;
@ -175,4 +175,25 @@ export module ValidatorsEx {
return null;
};
}
export function uniqueStrings(): ValidatorFn {
return (control: AbstractControl) => {
if (isEmptyInputValue(control.value) || !Types.isArrayOfString(control.value)) {
return null;
}
const a: string[] = control.value;
const unique: { [key: string]: boolean } = {};
for (let value of a) {
if (unique[value]) {
return { uniquestrings: false };
} else {
unique[value] = true;
}
}
return null;
};
}
}

2
src/Squidex/app/shared/services/schemas.types.ts

@ -181,6 +181,7 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto {
public readonly maxHeight?: number;
public readonly aspectWidth?: number;
public readonly aspectHeight?: number;
public readonly allowDuplicates?: boolean;
public get isSortable() {
return false;
@ -308,6 +309,7 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly schemaId?: string;
public readonly allowDuplicates?: boolean;
public get isSortable() {
return false;

4
src/Squidex/app/shared/state/contents.forms.spec.ts

@ -144,7 +144,7 @@ describe('AssetsField', () => {
const field = createField(new AssetsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(2);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {
@ -327,7 +327,7 @@ describe('ReferencesField', () => {
const field = createField(new ReferencesFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(2);
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);
});
it('should format to empty string if null', () => {

8
src/Squidex/app/shared/state/contents.forms.ts

@ -197,6 +197,10 @@ export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorF
ValidatorsEx.betweenLength(properties.minItems, properties.maxItems)
];
if (!properties.allowDuplicates) {
validators.push(ValidatorsEx.uniqueStrings());
}
return validators;
}
@ -205,6 +209,10 @@ export class FieldValidatorsFactory implements FieldPropertiesVisitor<ValidatorF
ValidatorsEx.betweenLength(properties.minItems, properties.maxItems)
];
if (!properties.allowDuplicates) {
validators.push(ValidatorsEx.uniqueStrings());
}
return validators;
}

6
src/Squidex/appsettings.json

@ -430,6 +430,10 @@
/*
* Set to true to rebuild schemas.
*/
"schemas": false
"schemas": false,
/*
* Set to true to rebuild indexes.
*/
"indexes": false
}
}

59
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -51,7 +51,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
PixelHeight = null
};
private readonly AssetInfo image = new AssetInfo
private readonly AssetInfo image1 = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyImage.png",
FileSize = 1024 * 8,
IsImage = true,
PixelWidth = 800,
PixelHeight = 600
};
private readonly AssetInfo image2 = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyImage.png",
@ -65,7 +75,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
public AssetsFieldTests()
{
ctx = ValidationTestExtensions.Assets(image, document);
ctx = ValidationTestExtensions.Assets(image1, image2, document);
}
[Fact]
@ -101,7 +111,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 });
await sut.ValidateAsync(CreateValue(document.AssetId, document.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_duplicate_values_are_ignored()
{
var sut = Field(new AssetsFieldProperties { AllowDuplicates = true });
await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx);
Assert.Empty(errors);
}
@ -144,7 +164,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MinItems = 3 });
await sut.ValidateAsync(CreateValue(document.AssetId, document.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." });
@ -155,7 +175,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MaxItems = 1 });
await sut.ValidateAsync(CreateValue(document.AssetId, document.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." });
@ -179,7 +199,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MinSize = 5 * 1024 });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." });
@ -190,7 +210,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MaxSize = 5 * 1024 });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." });
@ -201,18 +221,29 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MustBeImage = true });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[1]: Not an image." });
}
[Fact]
public async Task Should_add_error_if_values_contains_duplicate()
{
var sut = Field(new AssetsFieldProperties { MustBeImage = true });
await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." });
}
[Fact]
public async Task Should_add_error_if_image_width_is_too_small()
{
var sut = Field(new AssetsFieldProperties { MinWidth = 1000 });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." });
@ -223,7 +254,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MaxWidth = 700 });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." });
@ -234,7 +265,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MinHeight = 800 });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." });
@ -245,7 +276,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MaxHeight = 500 });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." });
@ -256,7 +287,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Aspect ratio not '1:1'." });
@ -267,7 +298,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") });
await sut.ValidateAsync(CreateValue(document.AssetId, image.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[]

21
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs

@ -61,6 +61,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_duplicate_values_are_allowed()
{
var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true });
await sut.ValidateAsync(CreateValue(ref1, ref1), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_references_are_required_and_null()
{
@ -127,6 +137,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { $"Contains invalid reference '{ref1}'." });
}
[Fact]
public async Task Should_add_error_if_reference_contains_duplicate_values()
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId });
await sut.ValidateAsync(CreateValue(ref1, ref1), errors, ValidationTestExtensions.References(ref1));
errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." });
}
private static IJsonValue CreateValue(params Guid[] ids)
{
return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray());

61
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs

@ -0,0 +1,61 @@
// ==========================================================================
// 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 FluentAssertions;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
public class UniqueValuesValidatorTests
{
private readonly List<string> errors = new List<string>();
[Fact]
public async Task Should_not_add_error_if_value_is_null()
{
var sut = new UniqueValuesValidator<int>();
await sut.ValidateAsync(null, errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_value_is_not_collection()
{
var sut = new UniqueValuesValidator<int>();
await sut.ValidateAsync("value", errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_array_contains_no_duplicates()
{
var sut = new UniqueValuesValidator<int>();
await sut.ValidateAsync(new[] { 1, 2, 3 }, errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_array_contains_duplicates()
{
var sut = new UniqueValuesValidator<int>();
await sut.ValidateAsync(new[] { 1, 2, 2, 3 }, errors);
errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." });
}
}
}

11
tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NodaTime;
using Squidex.Infrastructure.TestHelpers;
@ -81,5 +82,15 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
Assert.Equal(value, serialized);
}
[Fact]
public void Should_serialize_and_deserialize_dictionary()
{
var value = new Dictionary<string, string> { ["Description"] = "value" };
var serialized = value.SerializeAndDeserialize();
Assert.Equal(value, serialized);
}
}
}

2
tools/Migrate_00/Program.cs

@ -22,7 +22,7 @@ namespace Migrate_00
var collection = mongoDatabase.GetCollection<BsonDocument>("Events");
Console.Write("Migrate Indices.....");
Console.Write("Migrate indexes.....");
collection.Indexes.DropAll();

3
tools/Migrate_01/OldEvents/AssetRenamed.cs

@ -9,6 +9,7 @@ using System;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Migrate_01.OldEvents
{
@ -20,7 +21,7 @@ namespace Migrate_01.OldEvents
public IEvent Migrate()
{
return new AssetAnnotated { FileName = FileName };
return SimpleMapper.Map(this, new AssetAnnotated());
}
}
}

3
tools/Migrate_01/OldEvents/AssetTagged.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Migrate_01.OldEvents
{
@ -21,7 +22,7 @@ namespace Migrate_01.OldEvents
public IEvent Migrate()
{
return new AssetAnnotated { Tags = Tags };
return SimpleMapper.Map(this, new AssetAnnotated());
}
}
}

2
tools/Migrate_01/RebuildOptions.cs

@ -15,6 +15,8 @@ namespace Migrate_01
public bool Contents { get; set; }
public bool Indexes { get; set; }
public bool Rules { get; set; }
public bool Schemas { get; set; }

11
tools/Migrate_01/RebuildRunner.cs

@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Migrate_01.Migrations;
using Squidex.Infrastructure;
namespace Migrate_01
@ -15,15 +16,18 @@ namespace Migrate_01
public sealed class RebuildRunner
{
private readonly Rebuilder rebuilder;
private readonly PopulateGrainIndexes populateGrainIndexes;
private readonly RebuildOptions rebuildOptions;
public RebuildRunner(Rebuilder rebuilder, IOptions<RebuildOptions> rebuildOptions)
public RebuildRunner(Rebuilder rebuilder, IOptions<RebuildOptions> rebuildOptions, PopulateGrainIndexes populateGrainIndexes)
{
Guard.NotNull(rebuilder, nameof(rebuilder));
Guard.NotNull(rebuildOptions, nameof(rebuildOptions));
Guard.NotNull(populateGrainIndexes, nameof(populateGrainIndexes));
this.rebuilder = rebuilder;
this.rebuildOptions = rebuildOptions.Value;
this.populateGrainIndexes = populateGrainIndexes;
}
public async Task RunAsync(CancellationToken ct)
@ -52,6 +56,11 @@ namespace Migrate_01
{
await rebuilder.RebuildContentAsync(ct);
}
if (rebuildOptions.Indexes)
{
await populateGrainIndexes.UpdateAsync();
}
}
}
}

Loading…
Cancel
Save