diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index be1dbe7f9..3dd412601 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -17,7 +17,6 @@ namespace Squidex.Domain.Apps.Core.Apps { public sealed class LanguagesConfig : IFieldPartitioning { - public static readonly LanguagesConfig Empty = new LanguagesConfig(ImmutableDictionary.Empty, null, false); public static readonly LanguagesConfig English = Build(Language.EN); private readonly ImmutableDictionary languages; diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs index be7bed2ca..e0f3b3618 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -38,7 +38,10 @@ namespace Squidex.Domain.Apps.Core.Rules Guard.NotNull(action, nameof(action)); this.trigger = trigger; + this.trigger.Freeze(); + this.action = action; + this.action.Freeze(); } [Pure] @@ -69,6 +72,8 @@ namespace Squidex.Domain.Apps.Core.Rules throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); } + newTrigger.Freeze(); + return Clone(clone => { clone.trigger = newTrigger; @@ -85,6 +90,8 @@ namespace Squidex.Domain.Apps.Core.Rules throw new ArgumentException("New action has another type.", nameof(newAction)); } + newAction.Freeze(); + return Clone(clone => { clone.action = newAction; diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 27a09f421..4662de274 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -9,7 +9,6 @@ using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; @@ -19,8 +18,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.State public class AppState : DomainObjectState, IAppEntity { - private static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN); - [JsonProperty] public string Name { get; set; } @@ -37,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State public AppContributors Contributors { get; set; } = AppContributors.Empty; [JsonProperty] - public LanguagesConfig LanguagesConfig { get; set; } = English; + public LanguagesConfig LanguagesConfig { get; set; } = LanguagesConfig.English; [JsonProperty] public bool IsArchived { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index cf7b15ff0..e44af49bf 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetGrain : DomainObjectGrain, IAssetGrain + public class AssetGrain : SquidexDomainObjectGrain, IAssetGrain { public AssetGrain(IStore store) : base(store) diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs index 3ea5d0ad3..483c292a7 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -29,6 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Backup public sealed class BackupGrain : Grain, IBackupGrain { private const int MaxBackups = 10; + private static readonly Duration UpdateDuration = Duration.FromSeconds(1); private readonly IClock clock; private readonly IAssetStore assetStore; private readonly IEventDataFormatter eventDataFormatter; @@ -95,8 +96,6 @@ namespace Squidex.Domain.Apps.Entities.Backup private async Task CleanupAsync() { - var hasUpdated = false; - foreach (var job in state.Jobs) { if (!job.Stopped.HasValue) @@ -104,21 +103,38 @@ namespace Squidex.Domain.Apps.Entities.Backup await CleanupAsync(job); job.Stopped = clock.GetCurrentInstant(); - job.Failed = true; + job.IsFailed = true; - hasUpdated = true; + await WriteAsync(); } } - - if (hasUpdated) - { - await WriteAsync(); - } } private async Task CleanupAsync(BackupStateJob job) { - await backupArchiveLocation.DeleteArchiveAsync(job.Id); + try + { + await backupArchiveLocation.DeleteArchiveAsync(job.Id); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "deleteArchive") + .WriteProperty("status", "failed") + .WriteProperty("backupId", job.Id.ToString())); + } + + try + { + await assetStore.DeleteAsync(job.Id.ToString(), 0, null); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "deleteBackup") + .WriteProperty("status", "failed") + .WriteProperty("backupId", job.Id.ToString())); + } } public async Task RunAsync() @@ -138,7 +154,9 @@ namespace Squidex.Domain.Apps.Entities.Backup currentTask = new CancellationTokenSource(); currentJob = job; - state.Jobs.Add(job); + var lastTimestamp = job.Started; + + state.Jobs.Insert(0, job); await WriteAsync(); @@ -152,8 +170,8 @@ namespace Squidex.Domain.Apps.Entities.Backup { var eventData = @event.Data; - if (eventData.Type == nameof(AssetCreated) || - eventData.Type == nameof(AssetUpdated)) + if (eventData.Type == "AssetCreatedEvent" || + eventData.Type == "AssetUpdatedEvent") { var parsedEvent = eventDataFormatter.Parse(eventData); @@ -176,11 +194,24 @@ namespace Squidex.Domain.Apps.Entities.Backup { await assetStore.DownloadAsync(assetId.ToString(), assetVersion, null, attachmentStream); }); + + job.HandledAssets++; } else { await writer.WriteEventAsync(eventData); } + + job.HandledEvents++; + + var now = clock.GetCurrentInstant(); + + if ((now - lastTimestamp) >= UpdateDuration) + { + lastTimestamp = now; + + await WriteAsync(); + } }, SquidexHeaders.AppId, appId.ToString(), null, currentTask.Token); } @@ -189,16 +220,21 @@ namespace Squidex.Domain.Apps.Entities.Backup currentTask.Token.ThrowIfCancellationRequested(); await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream, currentTask.Token); - - currentTask.Token.ThrowIfCancellationRequested(); } } - catch + catch (Exception ex) { - job.Failed = true; + log.LogError(ex, w => w + .WriteProperty("action", "makeBackup") + .WriteProperty("status", "failed") + .WriteProperty("backupId", job.Id.ToString())); + + job.IsFailed = true; } finally { + await CleanupAsync(job); + job.Stopped = clock.GetCurrentInstant(); await WriteAsync(); diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs b/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs index 472c0e295..4142fd5e0 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs @@ -18,6 +18,10 @@ namespace Squidex.Domain.Apps.Entities.Backup Instant? Stopped { get; } - bool Failed { get; } + int HandledEvents { get; } + + int HandledAssets { get; } + + bool IsFailed { get; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs b/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs index 771843464..58c5d37b4 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs @@ -23,6 +23,12 @@ namespace Squidex.Domain.Apps.Entities.Backup.State public Instant? Stopped { get; set; } [JsonProperty] - public bool Failed { get; set; } + public int HandledEvents { get; set; } + + [JsonProperty] + public int HandledAssets { get; set; } + + [JsonProperty] + public bool IsFailed { get; set; } } } diff --git a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs index e19b9af8a..c5f841510 100644 --- a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs +++ b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs @@ -114,18 +114,25 @@ namespace Squidex.Infrastructure.Assets await blobRef.SetMetadataAsync(); } - public async Task UploadAsync(string name, Stream stream, CancellationToken ct = default(CancellationToken)) + public Task UploadAsync(string name, Stream stream, CancellationToken ct = default(CancellationToken)) { var tempBlob = blobContainer.GetBlockBlobReference(name); - await tempBlob.UploadFromStreamAsync(stream, null, null, null, ct); + return tempBlob.UploadFromStreamAsync(stream, null, null, null, ct); } - public async Task DeleteAsync(string name) + public Task DeleteAsync(string name) { var tempBlob = blobContainer.GetBlockBlobReference(name); - await tempBlob.DeleteIfExistsAsync(); + return tempBlob.DeleteIfExistsAsync(); + } + + public Task DeleteAsync(string id, long version, string suffix) + { + var tempBlob = blobContainer.GetBlockBlobReference(GetObjectName(id, version, suffix)); + + return tempBlob.DeleteIfExistsAsync(); } private string GetObjectName(string id, long version, string suffix) diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs index 8df8c6094..5af2be27a 100644 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -104,6 +104,21 @@ namespace Squidex.Infrastructure.Assets } } + public async Task DeleteAsync(string id, long version, string suffix) + { + try + { + await storageClient.DeleteObjectAsync(bucketName, GetObjectName(id, version, suffix)); + } + catch (GoogleApiException ex) + { + if (ex.HttpStatusCode != HttpStatusCode.NotFound) + { + throw; + } + } + } + private string GetObjectName(string id, long version, string suffix) { Guard.NotNullOrEmpty(id, nameof(id)); diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs index d908cbf24..6ff9d8598 100644 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -112,20 +112,22 @@ namespace Squidex.Infrastructure.Assets } } + public Task DeleteAsync(string id, long version, string suffix) + { + var file = GetFile(id, version, suffix); + + file.Delete(); + + return TaskHelper.Done; + } + public Task DeleteAsync(string name) { - try - { - var file = GetFile(name); + var file = GetFile(name); - file.Delete(); + file.Delete(); - return TaskHelper.Done; - } - catch (FileNotFoundException ex) - { - throw new AssetNotFoundException($"Asset {name} not found.", ex); - } + return TaskHelper.Done; } private FileInfo GetFile(string id, long version, string suffix) diff --git a/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/src/Squidex.Infrastructure/Assets/IAssetStore.cs index 9e8cd3bcd..7a4d2cdbc 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetStore.cs @@ -24,5 +24,7 @@ namespace Squidex.Infrastructure.Assets Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default(CancellationToken)); Task DeleteAsync(string name); + + Task DeleteAsync(string id, long version, string suffix); } } \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupsContentController.cs b/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs similarity index 90% rename from src/Squidex/Areas/Api/Controllers/Backups/BackupsContentController.cs rename to src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index b30d59f88..0db366eef 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/BackupsContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -17,15 +17,14 @@ namespace Squidex.Areas.Api.Controllers.Backups /// /// Manages backups for app. /// - [ApiAuthorize] [ApiExceptionFilter] [AppApi] [SwaggerTag(nameof(Backups))] - public class BackupsContentController : ApiController + public class BackupContentController : ApiController { private readonly IAssetStore assetStore; - public BackupsContentController(ICommandBus commandBus, IAssetStore assetStore) + public BackupContentController(ICommandBus commandBus, IAssetStore assetStore) : base(commandBus) { this.assetStore = assetStore; diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 158c3afdf..ad0389c07 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -161,6 +161,9 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + services.AddTransientAs() + .As(); + services.AddTransientAs() .As(); diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html index 766a8807c..6c7c35bf8 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -1,10 +1,10 @@ - +
-
@@ -23,10 +23,10 @@ Your have reached the maximum number of backups: 10.
-
-
+
+
-
+
@@ -37,32 +37,34 @@
-
Started:
-
Stopped:
+
+ Started: +
+
+ Duration: +
-
+
{{backup.started.toISOString()}}
- {{backup.stopped.toISOString()}} + {{getDuration(backup) | sqxDuration}}
-
-
Progress:
-
Download:
-
- {{backup.handledEvents}} + Events: {{backup.handledEvents | sqxKNumber}} , - {{backup.handledAssets}} + Assets: {{backup.handledAssets | sqxKNumber}}
-
- + diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss b/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss index 57b091787..80a10e112 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss @@ -6,12 +6,13 @@ $cicle-size: 2.8rem; .backup-status { & { @include circle($cicle-size); - line-height: $cicle-size; + line-height: $cicle-size + .1rem; text-align: center; font-size: .4 * $cicle-size; font-weight: normal; background: $color-border; color: $color-dark-foreground; + vertical-align: middle; } &-pending { diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts index 512485668..42a069426 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts @@ -14,6 +14,7 @@ import { BackupDto, BackupsService, DateTime, + Duration, ImmutableArray } from 'shared'; @@ -77,5 +78,13 @@ export class BackupsPageComponent implements OnInit, OnDestroy { public getDownloadUrl(backup: BackupDto) { return this.apiUrl.buildUrl(`api/apps/${this.ctx.appName}/backups/${backup.id}`); } + + public getDuration(backup: BackupDto) { + return Duration.create(backup.started, backup.stopped!); + } + + public trackBy(index: number, item: BackupDto) { + return item.id; + } } 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 c01bb8fec..276685d44 100644 --- a/src/Squidex/app/framework/angular/date-time.pipes.spec.ts +++ b/src/Squidex/app/framework/angular/date-time.pipes.spec.ts @@ -23,12 +23,12 @@ const dateTime = DateTime.parse('2013-10-03T12:13:14.125', DateTime.iso8601()); describe('DurationPipe', () => { it('should format to standard duration string', () => { - const duration = Duration.create(dateTime, dateTime.addMinutes(10).addDays(13)); + const duration = Duration.create(dateTime, dateTime.addMinutes(10).addDays(13).addSeconds(10)); const pipe = new DurationPipe(); const actual = pipe.transform(duration); - const expected = '312:10h'; + const expected = '312:10:10'; expect(actual).toBe(expected); }); diff --git a/src/Squidex/app/framework/utils/duration.spec.ts b/src/Squidex/app/framework/utils/duration.spec.ts index c71f25329..d5168fae0 100644 --- a/src/Squidex/app/framework/utils/duration.spec.ts +++ b/src/Squidex/app/framework/utils/duration.spec.ts @@ -28,24 +28,24 @@ describe('Duration', () => { it('should print to string correctly', () => { const s = DateTime.today(); - const d = s.addHours(1).addMinutes(30).addSeconds(60); + const d = s.addHours(12).addMinutes(30).addSeconds(60); const duration = Duration.create(s, d); const actual = duration.toString(); - const expected = '1:31h'; + const expected = '12:31:00'; expect(actual).toBe(expected); }); it('should print to string correctly for one digit minutes', () => { const s = DateTime.today(); - const d = s.addHours(1).addMinutes(1).addSeconds(60); + const d = s.addHours(1).addMinutes(2).addSeconds(5); const duration = Duration.create(s, d); const actual = duration.toString(); - const expected = '1:02h'; + const expected = '01:02:05'; expect(actual).toBe(expected); }); diff --git a/src/Squidex/app/framework/utils/duration.ts b/src/Squidex/app/framework/utils/duration.ts index 7369d65b0..558bb2ca1 100644 --- a/src/Squidex/app/framework/utils/duration.ts +++ b/src/Squidex/app/framework/utils/duration.ts @@ -25,12 +25,24 @@ export class Duration { public toString(): string { const duration = moment.duration(this.value); + let hoursString = Math.floor(duration.asHours()).toString(); + + if (hoursString.length === 1) { + hoursString = `0${hoursString}`; + } + let minutesString = duration.minutes().toString(); if (minutesString.length === 1) { minutesString = `0${minutesString}`; } - return Math.floor(duration.asHours()) + ':' + minutesString + 'h'; + let secondsString = duration.seconds().toString(); + + if (secondsString.length === 1) { + secondsString = `0${secondsString}`; + } + + return `${hoursString}:${minutesString}:${secondsString}`; } } \ No newline at end of file diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index 9705b059d..6affb6d9e 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -47,7 +47,7 @@ if ($all -Or $appsEntities) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj" ` - -filter:"+[Squidex.Domain.Apps.Entities*]*" ` + -filter:"+[Squidex.Domain.Apps.Entities*]* -[Squidex.Domain.Apps.Entities*]*CodeGen*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Entities.xml" ` -oldStyle diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 6a6e6028e..c8addbb71 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -37,6 +37,36 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.True(result is JObject); } + [Fact] + public void Should_create_route_data() + { + var appId = Guid.NewGuid(); + + var @event = new ContentCreated + { + AppId = new NamedId(appId, "my-app") + }; + + var result = sut.ToRouteData(AsEnvelope(@event)); + + Assert.True(result is JObject); + } + + [Fact] + public void Should_create_route_data_from_event() + { + var appId = Guid.NewGuid(); + + var @event = new ContentCreated + { + AppId = new NamedId(appId, "my-app") + }; + + var result = sut.ToRouteData(AsEnvelope(@event), "MyEventName"); + + Assert.Equal("MyEventName", result["type"]); + } + [Fact] public void Should_replace_app_information_from_event() { @@ -165,6 +195,23 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal("Berlin", result); } + [Fact] + public void Should_return_plain_value_when_found_from_update_event() + { + var @event = new ContentUpdated + { + Data = + new NamedContentData() + .AddField("city", + new ContentFieldData() + .AddValue("iv", "Berlin")) + }; + + var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); + + Assert.Equal("Berlin", result); + } + [Fact] public void Should_return_undefined_when_null() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 9f18d94e5..57122aa8c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Fact] - public void Should_not_create_trigger_if_no_trigger_handler_registered() + public void Should_not_create_job_if_no_trigger_handler_registered() { var ruleConfig = new Rule(new InvalidTrigger(), new WebhookAction()); var ruleEnvelope = Envelope.Create(new ContentCreated()); @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Fact] - public void Should_not_create_trigger_if_no_action_handler_registered() + public void Should_not_create_job_if_no_action_handler_registered() { var ruleConfig = new Rule(new ContentChangedTrigger(), new InvalidAction()); var ruleEnvelope = Envelope.Create(new ContentCreated()); @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_not_create_if_not_triggered() { - var ruleConfig = new Rule(new ContentChangedTrigger(), new InvalidAction()); + var ruleConfig = new Rule(new ContentChangedTrigger(), new WebhookAction()); var ruleEnvelope = Envelope.Create(new ContentCreated()); A.CallTo(() => ruleTriggerHandler.Triggers(A>.Ignored, ruleConfig.Trigger)) diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs index 657331efc..f8f0923d8 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs @@ -84,7 +84,7 @@ namespace Squidex.Infrastructure.Assets } [Fact] - public async Task Should_ignore_when_deleting_twice() + public async Task Should_ignore_when_deleting_twice_by_name() { ((IInitializable)Sut).Initialize(); @@ -97,6 +97,20 @@ namespace Squidex.Infrastructure.Assets await Sut.DeleteAsync(tempId); } + [Fact] + public async Task Should_ignore_when_deleting_twice_by_id() + { + ((IInitializable)Sut).Initialize(); + + var tempId = Id(); + + var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); + + await Sut.UploadAsync(tempId, 0, null, assetData); + await Sut.DeleteAsync(tempId, 0, null); + await Sut.DeleteAsync(tempId, 0, null); + } + private static string Id() { return Guid.NewGuid().ToString(); diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs index b408df447..ff7efb52d 100644 --- a/tools/Migrate_01/MigrationPath.cs +++ b/tools/Migrate_01/MigrationPath.cs @@ -15,7 +15,7 @@ namespace Migrate_01 { public sealed class MigrationPath : IMigrationPath { - private const int CurrentVersion = 6; + private const int CurrentVersion = 7; private readonly IServiceProvider serviceProvider; public MigrationPath(IServiceProvider serviceProvider) @@ -38,6 +38,12 @@ namespace Migrate_01 migrations.Add(serviceProvider.GetRequiredService()); } + // Version 7: Introduces AppId for backups. + else if (version < 7) + { + migrations.Add(serviceProvider.GetRequiredService()); + } + // Version 5: Fixes the broken command architecture and requires a rebuild of all snapshots. if (version < 5) { diff --git a/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs b/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs new file mode 100644 index 000000000..ad10f77d0 --- /dev/null +++ b/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// 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; + +namespace Migrate_01.Migrations +{ + public sealed class ConvertEventStoreAppId : IMigration + { + private readonly IEventStore eventStore; + + public ConvertEventStoreAppId(IEventStore eventStore) + { + this.eventStore = eventStore; + } + + public async Task UpdateAsync() + { + 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) + { + var data = JObject.Parse(@event["Payload"].AsString); + + if (data.TryGetValue("appId", out var appId)) + { + @event["Metadata"][SquidexHeaders.AppId] = NamedId.Parse(appId.ToString(), Guid.TryParse).Id.ToString(); + } + } + + await collection.ReplaceOneAsync(filter.Eq("_id", commit["_id"].AsString), commit); + }); + } + } + } +}