mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
118 changed files with 1217 additions and 349 deletions
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps; |
||||
|
|
||||
|
public sealed class AppsOptions |
||||
|
{ |
||||
|
public bool DeletePermanent { get; set; } |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents; |
||||
|
|
||||
|
public sealed class ContentEventDeleter(IContentRepository contentRepository, IEventStore eventStore) : IDeleter |
||||
|
{ |
||||
|
public int Order => -1000; |
||||
|
|
||||
|
public Task DeleteAppAsync(App app, CancellationToken ct) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public async Task DeleteSchemAsync(App app, Schema schema, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
await foreach (var id in contentRepository.StreamIds(app.Id, schema.Id, SearchScope.All, ct)) |
||||
|
{ |
||||
|
var streamFilter = StreamFilter.Prefix($"content-{DomainId.Combine(app.Id, id)}"); |
||||
|
|
||||
|
await eventStore.DeleteAsync(streamFilter, ct); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas.DomainObject; |
||||
|
using Squidex.Domain.Apps.Events.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
public sealed class SchemaPermanentDeleter( |
||||
|
IAppProvider appProvider, |
||||
|
IEnumerable<IDeleter> deleters, |
||||
|
IOptions<SchemasOptions> options, |
||||
|
IDomainObjectFactory factory, |
||||
|
TypeRegistry typeRegistry) |
||||
|
: IEventConsumer |
||||
|
{ |
||||
|
private readonly IEnumerable<IDeleter> deleters = deleters.OrderBy(x => x.Order).ToList(); |
||||
|
private readonly SchemasOptions options = options.Value; |
||||
|
private readonly HashSet<string> consumingTypes = |
||||
|
[ |
||||
|
typeRegistry.GetName<IEvent, SchemaDeleted>(), |
||||
|
]; |
||||
|
|
||||
|
public StreamFilter EventsFilter { get; } = StreamFilter.Prefix("schema-"); |
||||
|
|
||||
|
public ValueTask<bool> HandlesAsync(StoredEvent @event) |
||||
|
{ |
||||
|
return new ValueTask<bool>(consumingTypes.Contains(@event.Data.Type)); |
||||
|
} |
||||
|
|
||||
|
public async Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
if (@event.Headers.Restored()) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
switch (@event.Payload) |
||||
|
{ |
||||
|
case SchemaDeleted schemaDeleted: |
||||
|
await OnDeleteAsync(schemaDeleted); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task OnDeleteAsync(SchemaDeleted schemaDeleted) |
||||
|
{ |
||||
|
// The user can either remove the app itself or via a global setting for all apps.
|
||||
|
if (!schemaDeleted.Permanent && !options.DeletePermanent) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
using var activity = Telemetry.Activities.StartActivity("RemoveAppFromSystem"); |
||||
|
|
||||
|
var app = await appProvider.GetAppAsync(schemaDeleted.AppId.Id); |
||||
|
if (app == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var schema = await GetSchemaAsync(app.Id, schemaDeleted.SchemaId.Id); |
||||
|
if (schema == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
foreach (var deleter in deleters) |
||||
|
{ |
||||
|
using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) |
||||
|
{ |
||||
|
await deleter.DeleteSchemaAsync(app, schema.Snapshot, default); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task<SchemaDomainObject?> GetSchemaAsync(DomainId appId, DomainId schemaId) |
||||
|
{ |
||||
|
// Bypass our normal resolve process, so that we can also retrieve the deleted schema.
|
||||
|
var schema = factory.Create<SchemaDomainObject>(DomainId.Combine(appId, schemaId)); |
||||
|
|
||||
|
await schema.EnsureLoadedAsync(); |
||||
|
// If the app does not exist, the version is lower than zero.
|
||||
|
return schema.Version < 0 ? null : schema; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
public sealed class SchemasOptions |
||||
|
{ |
||||
|
public bool DeletePermanent { get; set; } |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Web; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Apps.Models; |
||||
|
|
||||
|
[OpenApiRequest] |
||||
|
public sealed class DeleteAppDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// True to delete the app permanently.
|
||||
|
/// </summary>
|
||||
|
[FromQuery(Name = "permanent")] |
||||
|
public bool Permanent { get; set; } |
||||
|
|
||||
|
public DeleteApp ToCommand() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new DeleteApp()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas.Commands; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Web; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Schemas.Models; |
||||
|
|
||||
|
[OpenApiRequest] |
||||
|
public sealed class DeleteSchemaDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// True to delete the schema and the contents permanently.
|
||||
|
/// </summary>
|
||||
|
[FromQuery(Name = "permanent")] |
||||
|
public bool Permanent { get; set; } |
||||
|
|
||||
|
public DeleteSchema ToCommand() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new DeleteSchema()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents; |
||||
|
|
||||
|
public class ContentEventDeleterTests : GivenContext |
||||
|
{ |
||||
|
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); |
||||
|
private readonly IEventStore eventStore = A.Fake<IEventStore>(); |
||||
|
private readonly ContentEventDeleter sut; |
||||
|
|
||||
|
public ContentEventDeleterTests() |
||||
|
{ |
||||
|
sut = new ContentEventDeleter(contentRepository, eventStore); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_run_at_beginning() |
||||
|
{ |
||||
|
var order = sut.Order; |
||||
|
|
||||
|
Assert.Equal(-1000, order); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_do_nothing_when_app_deleted() |
||||
|
{ |
||||
|
await sut.DeleteAppAsync(App, CancellationToken); |
||||
|
|
||||
|
A.CallTo(eventStore) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_remove_events_from_streams() |
||||
|
{ |
||||
|
var id1 = DomainId.NewGuid(); |
||||
|
var id2 = DomainId.NewGuid(); |
||||
|
var ids = new[] { id1, id2 }; |
||||
|
|
||||
|
A.CallTo(() => contentRepository.StreamIds(App.Id, Schema.Id, SearchScope.All, CancellationToken)) |
||||
|
.Returns(ids.ToAsyncEnumerable()); |
||||
|
|
||||
|
await sut.DeleteSchemAsync(App, Schema, CancellationToken); |
||||
|
|
||||
|
A.CallTo(() => eventStore.DeleteAsync( |
||||
|
A<StreamFilter>.That.Matches(x => x.Prefixes!.Contains($"content-{AppId.Id}--{id1}")), |
||||
|
CancellationToken)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => eventStore.DeleteAsync( |
||||
|
A<StreamFilter>.That.Matches(x => x.Prefixes!.Contains($"content-{AppId.Id}--{id2}")), |
||||
|
CancellationToken)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue