mirror of https://github.com/Squidex/squidex.git
Browse Source
* Update deps * Temp * First version. * More tests * Simplified tests. * Improve tests. * More fixes. * Permissions checks.pull/916/head
committed by
GitHub
73 changed files with 2259 additions and 416 deletions
@ -0,0 +1,22 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Squidex.Messaging.Subscriptions; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Subscriptions |
||||
|
{ |
||||
|
public abstract class AppSubscription : ISubscription |
||||
|
{ |
||||
|
public DomainId AppId { get; set; } |
||||
|
|
||||
|
public PermissionSet Permissions { get; set; } |
||||
|
|
||||
|
public abstract ValueTask<bool> ShouldHandle(object message); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Events.Assets; |
||||
|
using Squidex.Shared; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Subscriptions |
||||
|
{ |
||||
|
public sealed class AssetSubscription : AppSubscription |
||||
|
{ |
||||
|
public EnrichedAssetEventType? Type { get; set; } |
||||
|
|
||||
|
public override ValueTask<bool> ShouldHandle(object message) |
||||
|
{ |
||||
|
return new ValueTask<bool>(ShouldHandleCore(message)); |
||||
|
} |
||||
|
|
||||
|
private bool ShouldHandleCore(object message) |
||||
|
{ |
||||
|
switch (message) |
||||
|
{ |
||||
|
case EnrichedAssetEvent enrichedAssetEvent: |
||||
|
return ShouldHandle(enrichedAssetEvent); |
||||
|
case AssetEvent assetEvent: |
||||
|
return ShouldHandle(assetEvent); |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool ShouldHandle(EnrichedAssetEvent @event) |
||||
|
{ |
||||
|
return CheckType(@event) && CheckPermission(@event.AppId.Name); |
||||
|
} |
||||
|
|
||||
|
private bool ShouldHandle(AssetEvent @event) |
||||
|
{ |
||||
|
return CheckType(@event) && CheckPermission(@event.AppId.Name); |
||||
|
} |
||||
|
|
||||
|
private bool CheckType(EnrichedAssetEvent @event) |
||||
|
{ |
||||
|
return Type == null || Type.Value == @event.Type; |
||||
|
} |
||||
|
|
||||
|
private bool CheckType(AssetEvent @event) |
||||
|
{ |
||||
|
switch (Type) |
||||
|
{ |
||||
|
case EnrichedAssetEventType.Created: |
||||
|
return @event is AssetCreated; |
||||
|
case EnrichedAssetEventType.Deleted: |
||||
|
return @event is AssetDeleted; |
||||
|
case EnrichedAssetEventType.Annotated: |
||||
|
return @event is AssetAnnotated; |
||||
|
case EnrichedAssetEventType.Updated: |
||||
|
return @event is AssetUpdated; |
||||
|
default: |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool CheckPermission(string appName) |
||||
|
{ |
||||
|
var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appName); |
||||
|
|
||||
|
return Permissions.Includes(permission); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Shared; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Subscriptions |
||||
|
{ |
||||
|
public sealed class ContentSubscription : AppSubscription |
||||
|
{ |
||||
|
public string? SchemaName { get; set; } |
||||
|
|
||||
|
public EnrichedContentEventType? Type { get; set; } |
||||
|
|
||||
|
public override ValueTask<bool> ShouldHandle(object message) |
||||
|
{ |
||||
|
return new ValueTask<bool>(ShouldHandleCore(message)); |
||||
|
} |
||||
|
|
||||
|
private bool ShouldHandleCore(object message) |
||||
|
{ |
||||
|
switch (message) |
||||
|
{ |
||||
|
case EnrichedContentEvent enrichedContentEvent: |
||||
|
return ShouldHandle(enrichedContentEvent); |
||||
|
case ContentEvent contentEvent: |
||||
|
return ShouldHandle(contentEvent); |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool ShouldHandle(EnrichedContentEvent @event) |
||||
|
{ |
||||
|
var schemaName = @event.SchemaId.Name; |
||||
|
|
||||
|
return CheckSchema(schemaName) && CheckType(@event) && CheckPermission(@event.AppId.Name, schemaName); |
||||
|
} |
||||
|
|
||||
|
private bool ShouldHandle(ContentEvent @event) |
||||
|
{ |
||||
|
var schemaName = @event.SchemaId.Name; |
||||
|
|
||||
|
return CheckSchema(schemaName) && CheckType(@event) && CheckPermission(@event.AppId.Name, schemaName); |
||||
|
} |
||||
|
|
||||
|
private bool CheckSchema(string schemaName) |
||||
|
{ |
||||
|
return string.IsNullOrWhiteSpace(SchemaName) || schemaName == SchemaName; |
||||
|
} |
||||
|
|
||||
|
private bool CheckType(EnrichedContentEvent @event) |
||||
|
{ |
||||
|
return Type == null || Type.Value == @event.Type; |
||||
|
} |
||||
|
|
||||
|
private bool CheckType(ContentEvent @event) |
||||
|
{ |
||||
|
switch (Type) |
||||
|
{ |
||||
|
case EnrichedContentEventType.Created: |
||||
|
return @event is ContentCreated; |
||||
|
case EnrichedContentEventType.Deleted: |
||||
|
return @event is ContentDeleted; |
||||
|
case EnrichedContentEventType.Published: |
||||
|
return @event is ContentStatusChanged { Change: Contents.StatusChange.Published }; |
||||
|
case EnrichedContentEventType.Unpublished: |
||||
|
return @event is ContentStatusChanged { Change: Contents.StatusChange.Unpublished }; |
||||
|
case EnrichedContentEventType.StatusChanged: |
||||
|
return @event is ContentStatusChanged { Change: Contents.StatusChange.Change }; |
||||
|
case EnrichedContentEventType.Updated: |
||||
|
return @event is ContentUpdated; |
||||
|
default: |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool CheckPermission(string appName, string schemaName) |
||||
|
{ |
||||
|
var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, appName, schemaName); |
||||
|
|
||||
|
return Permissions.Allows(permission); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Messaging.Subscriptions; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Subscriptions |
||||
|
{ |
||||
|
public sealed class EventMessageEvaluator : IMessageEvaluator |
||||
|
{ |
||||
|
private readonly Dictionary<DomainId, Dictionary<Guid, AppSubscription>> subscriptions = new Dictionary<DomainId, Dictionary<Guid, AppSubscription>>(); |
||||
|
private readonly ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim(); |
||||
|
|
||||
|
public async ValueTask<IEnumerable<Guid>> GetSubscriptionsAsync(object message) |
||||
|
{ |
||||
|
if (message is not AppEvent appEvent) |
||||
|
{ |
||||
|
return Enumerable.Empty<Guid>(); |
||||
|
} |
||||
|
|
||||
|
readerWriterLock.EnterReadLock(); |
||||
|
try |
||||
|
{ |
||||
|
List<Guid>? result = null; |
||||
|
|
||||
|
if (subscriptions.TryGetValue(appEvent.AppId.Id, out var appSubscriptions)) |
||||
|
{ |
||||
|
foreach (var (id, subscription) in appSubscriptions) |
||||
|
{ |
||||
|
if (await subscription.ShouldHandle(appEvent)) |
||||
|
{ |
||||
|
result ??= new List<Guid>(); |
||||
|
result.Add(id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result ?? Enumerable.Empty<Guid>(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
readerWriterLock.ExitReadLock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SubscriptionAdded(Guid id, ISubscription subscription) |
||||
|
{ |
||||
|
if (subscription is not AppSubscription appSubscription) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
readerWriterLock.EnterWriteLock(); |
||||
|
try |
||||
|
{ |
||||
|
subscriptions.GetOrAddNew(appSubscription.AppId)[id] = appSubscription; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
readerWriterLock.ExitWriteLock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SubscriptionRemoved(Guid id, ISubscription subscription) |
||||
|
{ |
||||
|
if (subscription is not AppSubscription appSubscription) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
readerWriterLock.EnterWriteLock(); |
||||
|
try |
||||
|
{ |
||||
|
subscriptions.GetOrAddDefault(appSubscription.AppId)?.Remove(id); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
readerWriterLock.ExitWriteLock(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Messaging.Subscriptions; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Subscriptions |
||||
|
{ |
||||
|
public sealed class EventMessageWrapper : IPayloadWrapper |
||||
|
{ |
||||
|
private readonly IEnumerable<ISubscriptionEventCreator> subscriptionEventCreators; |
||||
|
|
||||
|
public Envelope<AppEvent> Event { get; } |
||||
|
|
||||
|
object IPayloadWrapper.Message => Event.Payload; |
||||
|
|
||||
|
public EventMessageWrapper(Envelope<AppEvent> @event, IEnumerable<ISubscriptionEventCreator> subscriptionEventCreators) |
||||
|
{ |
||||
|
Event = @event; |
||||
|
|
||||
|
this.subscriptionEventCreators = subscriptionEventCreators; |
||||
|
} |
||||
|
|
||||
|
public async ValueTask<object> CreatePayloadAsync() |
||||
|
{ |
||||
|
foreach (var creator in subscriptionEventCreators) |
||||
|
{ |
||||
|
if (!creator.Handles(Event.Payload)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var result = await creator.CreateEnrichedEventsAsync(Event, default); |
||||
|
|
||||
|
if (result != null) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null!; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Subscriptions |
||||
|
{ |
||||
|
public interface ISubscriptionEventCreator |
||||
|
{ |
||||
|
bool Handles(AppEvent @event); |
||||
|
|
||||
|
ValueTask<EnrichedEvent?> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, |
||||
|
CancellationToken ct); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Messaging.Subscriptions; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Subscriptions |
||||
|
{ |
||||
|
public sealed class SubscriptionPublisher : IEventConsumer |
||||
|
{ |
||||
|
private readonly ISubscriptionService subscriptionService; |
||||
|
private readonly IEnumerable<ISubscriptionEventCreator> subscriptionEventCreators; |
||||
|
|
||||
|
public string Name |
||||
|
{ |
||||
|
get => "Subscriptions"; |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get => "^(content-|asset-)"; |
||||
|
} |
||||
|
|
||||
|
public bool StartLatest |
||||
|
{ |
||||
|
get => true; |
||||
|
} |
||||
|
|
||||
|
public bool CanClear |
||||
|
{ |
||||
|
get => false; |
||||
|
} |
||||
|
|
||||
|
public SubscriptionPublisher(ISubscriptionService subscriptionService, IEnumerable<ISubscriptionEventCreator> subscriptionEventCreators) |
||||
|
{ |
||||
|
this.subscriptionService = subscriptionService; |
||||
|
this.subscriptionEventCreators = subscriptionEventCreators; |
||||
|
} |
||||
|
|
||||
|
public bool Handles(StoredEvent @event) |
||||
|
{ |
||||
|
return subscriptionService.HasSubscriptions; |
||||
|
} |
||||
|
|
||||
|
public Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
if (@event.Payload is not AppEvent) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
var wrapper = new EventMessageWrapper(@event.To<AppEvent>(), subscriptionEventCreators); |
||||
|
|
||||
|
return subscriptionService.PublishAsync(wrapper); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
||||
|
{ |
||||
|
internal sealed class ApplicationSubscriptions : ObjectGraphType |
||||
|
{ |
||||
|
public ApplicationSubscriptions() |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = $"assetChanges", |
||||
|
Arguments = AssetActions.Subscription.Arguments, |
||||
|
ResolvedType = SharedTypes.EnrichedAssetEvent, |
||||
|
Resolver = null, |
||||
|
StreamResolver = AssetActions.Subscription.Resolver, |
||||
|
Description = "Subscribe to asset events." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = $"contentChanges", |
||||
|
Arguments = ContentActions.Subscription.Arguments, |
||||
|
ResolvedType = SharedTypes.EnrichedContentEvent, |
||||
|
Resolver = null, |
||||
|
StreamResolver = ContentActions.Subscription.Resolver, |
||||
|
Description = "Subscribe to content events." |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,263 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using GraphQL; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Assets; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets |
||||
|
{ |
||||
|
internal sealed class EnrichedAssetEventGraphType : SharedObjectGraphType<EnrichedAssetEvent> |
||||
|
{ |
||||
|
public EnrichedAssetEventGraphType() |
||||
|
{ |
||||
|
// The name is used for equal comparison. Therefore it is important to treat it as readonly.
|
||||
|
Name = "EnrichedAssetEvent"; |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "type", |
||||
|
ResolvedType = Scalars.EnrichedAssetEventType, |
||||
|
Resolver = Resolve(x => x.Type), |
||||
|
Description = FieldDescriptions.EventType |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "id", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.Id.ToString()), |
||||
|
Description = FieldDescriptions.EntityId |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "version", |
||||
|
ResolvedType = Scalars.NonNullInt, |
||||
|
Resolver = Resolve(x => x.Version), |
||||
|
Description = FieldDescriptions.EntityVersion |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "created", |
||||
|
ResolvedType = Scalars.NonNullDateTime, |
||||
|
Resolver = Resolve(x => x.Created.ToDateTimeUtc()), |
||||
|
Description = FieldDescriptions.EntityCreated |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "createdBy", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.CreatedBy.ToString()), |
||||
|
Description = FieldDescriptions.EntityCreatedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "createdByUser", |
||||
|
ResolvedType = UserGraphType.NonNull, |
||||
|
Resolver = Resolve(x => x.CreatedBy), |
||||
|
Description = FieldDescriptions.EntityCreatedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModified", |
||||
|
ResolvedType = Scalars.NonNullDateTime, |
||||
|
Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()), |
||||
|
Description = FieldDescriptions.EntityLastModified |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModifiedBy", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.LastModifiedBy.ToString()), |
||||
|
Description = FieldDescriptions.EntityLastModifiedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModifiedByUser", |
||||
|
ResolvedType = UserGraphType.NonNull, |
||||
|
Resolver = Resolve(x => x.LastModifiedBy), |
||||
|
Description = FieldDescriptions.EntityLastModifiedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "mimeType", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.MimeType), |
||||
|
Description = FieldDescriptions.AssetMimeType |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "url", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Url, |
||||
|
Description = FieldDescriptions.AssetUrl |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "thumbnailUrl", |
||||
|
ResolvedType = Scalars.String, |
||||
|
Resolver = ThumbnailUrl, |
||||
|
Description = FieldDescriptions.AssetThumbnailUrl |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileName", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.FileName), |
||||
|
Description = FieldDescriptions.AssetFileName |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileHash", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.FileHash), |
||||
|
Description = FieldDescriptions.AssetFileHash |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileType", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.FileName.FileType()), |
||||
|
Description = FieldDescriptions.AssetFileType |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileSize", |
||||
|
ResolvedType = Scalars.NonNullInt, |
||||
|
Resolver = Resolve(x => x.FileSize), |
||||
|
Description = FieldDescriptions.AssetFileSize |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileVersion", |
||||
|
ResolvedType = Scalars.NonNullInt, |
||||
|
Resolver = Resolve(x => x.FileVersion), |
||||
|
Description = FieldDescriptions.AssetFileVersion |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "slug", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.Slug), |
||||
|
Description = FieldDescriptions.AssetSlug |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "isProtected", |
||||
|
ResolvedType = Scalars.NonNullBoolean, |
||||
|
Resolver = Resolve(x => x.IsProtected), |
||||
|
Description = FieldDescriptions.AssetIsProtected |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "isImage", |
||||
|
ResolvedType = Scalars.NonNullBoolean, |
||||
|
Resolver = Resolve(x => x.AssetType == AssetType.Image), |
||||
|
Description = FieldDescriptions.AssetIsImage, |
||||
|
DeprecationReason = "Use 'type' field instead." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "assetType", |
||||
|
ResolvedType = Scalars.NonNullAssetType, |
||||
|
Resolver = Resolve(x => x.AssetType), |
||||
|
Description = FieldDescriptions.AssetType |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "pixelWidth", |
||||
|
ResolvedType = Scalars.Int, |
||||
|
Resolver = Resolve(x => x.Metadata.GetPixelWidth()), |
||||
|
Description = FieldDescriptions.AssetPixelWidth, |
||||
|
DeprecationReason = "Use 'metadata' field instead." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "pixelHeight", |
||||
|
ResolvedType = Scalars.Int, |
||||
|
Resolver = Resolve(x => x.Metadata.GetPixelHeight()), |
||||
|
Description = FieldDescriptions.AssetPixelHeight, |
||||
|
DeprecationReason = "Use 'metadata' field instead." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "metadata", |
||||
|
Arguments = AssetActions.Metadata.Arguments, |
||||
|
ResolvedType = Scalars.JsonNoop, |
||||
|
Resolver = AssetActions.Metadata.Resolver, |
||||
|
Description = FieldDescriptions.AssetMetadata |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "sourceUrl", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = SourceUrl, |
||||
|
Description = FieldDescriptions.AssetSourceUrl |
||||
|
}); |
||||
|
|
||||
|
Description = "An asset event"; |
||||
|
} |
||||
|
|
||||
|
private static readonly IFieldResolver Url = Resolve((asset, _, context) => |
||||
|
{ |
||||
|
var urlGenerator = context.Resolve<IUrlGenerator>(); |
||||
|
|
||||
|
return urlGenerator.AssetContent(asset.AppId, asset.Id.ToString()); |
||||
|
}); |
||||
|
|
||||
|
private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => |
||||
|
{ |
||||
|
var urlGenerator = context.Resolve<IUrlGenerator>(); |
||||
|
|
||||
|
return urlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion); |
||||
|
}); |
||||
|
|
||||
|
private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => |
||||
|
{ |
||||
|
var urlGenerator = context.Resolve<IUrlGenerator>(); |
||||
|
|
||||
|
return urlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.AssetType); |
||||
|
}); |
||||
|
|
||||
|
private static IFieldResolver Resolve<T>(Func<EnrichedAssetEvent, IResolveFieldContext, GraphQLExecutionContext, T> resolver) |
||||
|
{ |
||||
|
return Resolvers.Sync(resolver); |
||||
|
} |
||||
|
|
||||
|
private static IFieldResolver Resolve<T>(Func<EnrichedAssetEvent, T> resolver) |
||||
|
{ |
||||
|
return Resolvers.Sync(resolver); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents |
||||
|
{ |
||||
|
internal sealed class EnrichedContentEventGraphType : SharedObjectGraphType<EnrichedContentEvent> |
||||
|
{ |
||||
|
public EnrichedContentEventGraphType() |
||||
|
{ |
||||
|
// The name is used for equal comparison. Therefore it is important to treat it as readonly.
|
||||
|
Name = "EnrichedContentEvent"; |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "type", |
||||
|
ResolvedType = Scalars.EnrichedContentEventType, |
||||
|
Resolver = Resolve(x => x.Type), |
||||
|
Description = FieldDescriptions.EventType |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "id", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.Id.ToString()), |
||||
|
Description = FieldDescriptions.EntityId |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "version", |
||||
|
ResolvedType = Scalars.NonNullInt, |
||||
|
Resolver = Resolve(x => x.Version), |
||||
|
Description = FieldDescriptions.EntityVersion |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "created", |
||||
|
ResolvedType = Scalars.NonNullDateTime, |
||||
|
Resolver = Resolve(x => x.Created.ToDateTimeUtc()), |
||||
|
Description = FieldDescriptions.EntityCreated |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "createdBy", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.CreatedBy.ToString()), |
||||
|
Description = FieldDescriptions.EntityCreatedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "createdByUser", |
||||
|
ResolvedType = UserGraphType.NonNull, |
||||
|
Resolver = Resolve(x => x.CreatedBy), |
||||
|
Description = FieldDescriptions.EntityCreatedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModified", |
||||
|
ResolvedType = Scalars.NonNullDateTime, |
||||
|
Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()), |
||||
|
Description = FieldDescriptions.EntityLastModified |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModifiedBy", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.LastModifiedBy.ToString()), |
||||
|
Description = FieldDescriptions.EntityLastModifiedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModifiedByUser", |
||||
|
ResolvedType = UserGraphType.NonNull, |
||||
|
Resolver = Resolve(x => x.LastModifiedBy), |
||||
|
Description = FieldDescriptions.EntityLastModifiedBy |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "status", |
||||
|
ResolvedType = Scalars.NonNullString, |
||||
|
Resolver = Resolve(x => x.Status.ToString()), |
||||
|
Description = FieldDescriptions.ContentStatus |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "newStatus", |
||||
|
ResolvedType = Scalars.String, |
||||
|
Resolver = Resolve(x => x.NewStatus?.ToString()), |
||||
|
Description = FieldDescriptions.ContentNewStatus |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "data", |
||||
|
ResolvedType = Scalars.JsonNoop, |
||||
|
Resolver = Resolve(x => x.Data), |
||||
|
Description = FieldDescriptions.ContentData |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "dataOld", |
||||
|
ResolvedType = Scalars.JsonNoop, |
||||
|
Resolver = Resolve(x => x.DataOld), |
||||
|
Description = FieldDescriptions.ContentDataOld |
||||
|
}); |
||||
|
|
||||
|
Description = "An content event"; |
||||
|
} |
||||
|
|
||||
|
private static IFieldResolver Resolve<T>(Func<EnrichedContentEvent, T> resolver) |
||||
|
{ |
||||
|
return Resolvers.Sync(resolver); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using GraphQL.Types; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
||||
|
{ |
||||
|
internal abstract class SharedObjectGraphType<T> : ObjectGraphType<T> |
||||
|
{ |
||||
|
public override void Initialize(ISchema schema) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
base.Initialize(schema); |
||||
|
} |
||||
|
catch (InvalidOperationException) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Subscriptions; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Domain.Apps.Events.Assets; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Squidex.Shared; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.Subscriptions |
||||
|
{ |
||||
|
public class AssetSubscriptionTests |
||||
|
{ |
||||
|
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_true_for_enriched_asset_event() |
||||
|
{ |
||||
|
var sut = WithPermission(new AssetSubscription()); |
||||
|
|
||||
|
var @event = Enrich(new EnrichedAssetEvent()); |
||||
|
|
||||
|
Assert.True(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_false_for_wrong_event() |
||||
|
{ |
||||
|
var sut = WithPermission(new AssetSubscription()); |
||||
|
|
||||
|
var @event = new AppCreated(); |
||||
|
|
||||
|
Assert.False(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_true_for_asset_event() |
||||
|
{ |
||||
|
var sut = WithPermission(new AssetSubscription()); |
||||
|
|
||||
|
var @event = Enrich(new AssetCreated()); |
||||
|
|
||||
|
Assert.True(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_true_for_asset_event_with_correct_type() |
||||
|
{ |
||||
|
var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Created }); |
||||
|
|
||||
|
var @event = Enrich(new AssetCreated()); |
||||
|
|
||||
|
Assert.True(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_false_for_asset_event_with_wrong_type() |
||||
|
{ |
||||
|
var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Deleted }); |
||||
|
|
||||
|
var @event = Enrich(new AssetCreated()); |
||||
|
|
||||
|
Assert.False(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_false_for_asset_event_invalid_permissions() |
||||
|
{ |
||||
|
var sut = WithPermission(new AssetSubscription(), PermissionIds.AppCommentsCreate); |
||||
|
|
||||
|
var @event = Enrich(new AssetCreated()); |
||||
|
|
||||
|
Assert.False(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
private object Enrich(EnrichedAssetEvent source) |
||||
|
{ |
||||
|
source.AppId = appId; |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
private object Enrich(AssetEvent source) |
||||
|
{ |
||||
|
source.AppId = appId; |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
private AssetSubscription WithPermission(AssetSubscription subscription, string? permissionId = null) |
||||
|
{ |
||||
|
subscription.AppId = appId.Id; |
||||
|
|
||||
|
permissionId ??= PermissionIds.AppAssetsRead; |
||||
|
|
||||
|
var permission = PermissionIds.ForApp(permissionId, appId.Name); |
||||
|
var permissions = new PermissionSet(permission); |
||||
|
|
||||
|
subscription.Permissions = permissions; |
||||
|
|
||||
|
return subscription; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Subscriptions; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Squidex.Shared; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.Subscriptions |
||||
|
{ |
||||
|
public class ContentSubscriptionTests |
||||
|
{ |
||||
|
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); |
||||
|
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_true_for_enriched_content_event() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription()); |
||||
|
|
||||
|
var @event = Enrich(new EnrichedContentEvent()); |
||||
|
|
||||
|
Assert.True(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_false_for_wrong_event() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription()); |
||||
|
|
||||
|
var @event = new AppCreated { AppId = appId }; |
||||
|
|
||||
|
Assert.False(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_true_for_content_event() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription()); |
||||
|
|
||||
|
var @event = Enrich(new ContentCreated()); |
||||
|
|
||||
|
Assert.True(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_true_for_content_event_with_correct_type() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription { Type = EnrichedContentEventType.Created }); |
||||
|
|
||||
|
var @event = Enrich(new ContentCreated()); |
||||
|
|
||||
|
Assert.True(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_false_for_content_event_with_wrong_type() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription { Type = EnrichedContentEventType.Deleted }); |
||||
|
|
||||
|
var @event = Enrich(new ContentCreated()); |
||||
|
|
||||
|
Assert.False(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_true_for_content_event_with_correct_schema() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription { SchemaName = schemaId.Name }); |
||||
|
|
||||
|
var @event = Enrich(new ContentCreated()); |
||||
|
|
||||
|
Assert.True(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_false_for_content_event_with_wrong_schema() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription { SchemaName = "wrong-schema" }); |
||||
|
|
||||
|
var @event = Enrich(new ContentCreated()); |
||||
|
|
||||
|
Assert.False(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_false_for_content_event_invalid_permissions() |
||||
|
{ |
||||
|
var sut = WithPermission(new ContentSubscription(), PermissionIds.AppCommentsCreate); |
||||
|
|
||||
|
var @event = Enrich(new ContentCreated()); |
||||
|
|
||||
|
Assert.False(await sut.ShouldHandle(@event)); |
||||
|
} |
||||
|
|
||||
|
private object Enrich(EnrichedContentEvent source) |
||||
|
{ |
||||
|
source.AppId = appId; |
||||
|
source.SchemaId = schemaId; |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
private object Enrich(ContentEvent source) |
||||
|
{ |
||||
|
source.AppId = appId; |
||||
|
source.SchemaId = schemaId; |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
private ContentSubscription WithPermission(ContentSubscription subscription, string? permissionId = null) |
||||
|
{ |
||||
|
subscription.AppId = appId.Id; |
||||
|
|
||||
|
permissionId ??= PermissionIds.AppContentsRead; |
||||
|
|
||||
|
var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); |
||||
|
var permissions = new PermissionSet(permission); |
||||
|
|
||||
|
subscription.Permissions = permissions; |
||||
|
|
||||
|
return subscription; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Subscriptions; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Domain.Apps.Events.Assets; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Squidex.Shared; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.Subscriptions |
||||
|
{ |
||||
|
public class EventMessageEvaluatorTests |
||||
|
{ |
||||
|
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); |
||||
|
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); |
||||
|
private readonly EventMessageEvaluator sut = new EventMessageEvaluator(); |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_empty_list_when_nothing_registered() |
||||
|
{ |
||||
|
var assetEvent = new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") }; |
||||
|
|
||||
|
var subscriptions = await sut.GetSubscriptionsAsync(assetEvent); |
||||
|
|
||||
|
Assert.Empty(subscriptions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_matching_subscriptions() |
||||
|
{ |
||||
|
var contentSubscriptionId = Guid.NewGuid(); |
||||
|
var contentSubscription = WithPermission(new ContentSubscription(), PermissionIds.AppContentsRead); |
||||
|
|
||||
|
var assetSubscriptionId = Guid.NewGuid(); |
||||
|
var assetSubscription = WithPermission(new AssetSubscription(), PermissionIds.AppAssetsRead); |
||||
|
|
||||
|
sut.SubscriptionAdded(contentSubscriptionId, contentSubscription); |
||||
|
sut.SubscriptionAdded(assetSubscriptionId, assetSubscription); |
||||
|
|
||||
|
Assert.Equal(new[] { contentSubscriptionId }, |
||||
|
await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); |
||||
|
|
||||
|
Assert.Equal(new[] { assetSubscriptionId }, |
||||
|
await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); |
||||
|
|
||||
|
Assert.Empty( |
||||
|
await sut.GetSubscriptionsAsync(Enrich(new AppCreated()))); |
||||
|
|
||||
|
Assert.Empty( |
||||
|
await sut.GetSubscriptionsAsync(new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") })); |
||||
|
|
||||
|
sut.SubscriptionRemoved(contentSubscriptionId, contentSubscription); |
||||
|
sut.SubscriptionRemoved(assetSubscriptionId, assetSubscription); |
||||
|
|
||||
|
Assert.Empty( |
||||
|
await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); |
||||
|
|
||||
|
Assert.Empty( |
||||
|
await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); |
||||
|
} |
||||
|
|
||||
|
private object Enrich(ContentEvent source) |
||||
|
{ |
||||
|
source.SchemaId = schemaId; |
||||
|
source.AppId = appId; |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
private object Enrich(AppEvent source) |
||||
|
{ |
||||
|
source.Actor = null!; |
||||
|
source.AppId = appId; |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
private AppSubscription WithPermission(AppSubscription subscription, string permissionId) |
||||
|
{ |
||||
|
subscription.AppId = appId.Id; |
||||
|
|
||||
|
var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); |
||||
|
var permissions = new PermissionSet(permission); |
||||
|
|
||||
|
subscription.Permissions = permissions; |
||||
|
|
||||
|
return subscription; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Subscriptions; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.Subscriptions |
||||
|
{ |
||||
|
public class EventMessageWrapperTests |
||||
|
{ |
||||
|
private readonly ISubscriptionEventCreator creator1 = A.Fake<ISubscriptionEventCreator>(); |
||||
|
private readonly ISubscriptionEventCreator creator2 = A.Fake<ISubscriptionEventCreator>(); |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_event_from_first_creator() |
||||
|
{ |
||||
|
var enrichedEvent = new EnrichedContentEvent(); |
||||
|
|
||||
|
var envelope = Envelope.Create<AppEvent>(new AppCreated()); |
||||
|
|
||||
|
A.CallTo(() => creator1.Handles(envelope.Payload)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
A.CallTo(() => creator1.CreateEnrichedEventsAsync(envelope, default)) |
||||
|
.Returns(null!); |
||||
|
|
||||
|
A.CallTo(() => creator2.Handles(envelope.Payload)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
A.CallTo(() => creator2.CreateEnrichedEventsAsync(envelope, default)) |
||||
|
.Returns(enrichedEvent); |
||||
|
|
||||
|
var sut = new EventMessageWrapper(envelope, new[] { creator1, creator2 }); |
||||
|
|
||||
|
var result = await sut.CreatePayloadAsync(); |
||||
|
|
||||
|
Assert.Same(enrichedEvent, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_invoke_creator_if_it_does_not_handle_event() |
||||
|
{ |
||||
|
var enrichedEvent = new EnrichedContentEvent(); |
||||
|
|
||||
|
var envelope = Envelope.Create<AppEvent>(new AppCreated()); |
||||
|
|
||||
|
A.CallTo(() => creator1.Handles(envelope.Payload)) |
||||
|
.Returns(false); |
||||
|
|
||||
|
var sut = new EventMessageWrapper(envelope, new[] { creator1 }); |
||||
|
|
||||
|
Assert.Null(await sut.CreatePayloadAsync()); |
||||
|
|
||||
|
A.CallTo(() => creator1.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>._, A<CancellationToken>._)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Subscriptions; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Messaging.Subscriptions; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.Subscriptions |
||||
|
{ |
||||
|
public class SubscriptionPublisherTests |
||||
|
{ |
||||
|
private readonly ISubscriptionService subscriptionService = A.Fake<ISubscriptionService>(); |
||||
|
private readonly SubscriptionPublisher sut; |
||||
|
|
||||
|
private sealed class MyEvent : IEvent |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public SubscriptionPublisherTests() |
||||
|
{ |
||||
|
sut = new SubscriptionPublisher(subscriptionService, Enumerable.Empty<ISubscriptionEventCreator>()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_content_and_asset_filter_for_events_filter() |
||||
|
{ |
||||
|
IEventConsumer consumer = sut; |
||||
|
|
||||
|
Assert.Equal("^(content-|asset-)", consumer.EventsFilter); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_do_nothing_on_clear() |
||||
|
{ |
||||
|
IEventConsumer consumer = sut; |
||||
|
|
||||
|
await consumer.ClearAsync(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_custom_name_for_name() |
||||
|
{ |
||||
|
IEventConsumer consumer = sut; |
||||
|
|
||||
|
Assert.Equal("Subscriptions", consumer.Name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_support_clear() |
||||
|
{ |
||||
|
IEventConsumer consumer = sut; |
||||
|
|
||||
|
Assert.False(consumer.CanClear); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_start_from_latest() |
||||
|
{ |
||||
|
IEventConsumer consumer = sut; |
||||
|
|
||||
|
Assert.True(consumer.StartLatest); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(true)] |
||||
|
[InlineData(false)] |
||||
|
public void Should_handle_events_when_subscription_exists(bool hasSubscriptions) |
||||
|
{ |
||||
|
A.CallTo(() => subscriptionService.HasSubscriptions) |
||||
|
.Returns(hasSubscriptions); |
||||
|
|
||||
|
IEventConsumer consumer = sut; |
||||
|
|
||||
|
Assert.Equal(hasSubscriptions, consumer.Handles(null!)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_publish_if_not_app_event() |
||||
|
{ |
||||
|
var envelope = Envelope.Create(new MyEvent()); |
||||
|
|
||||
|
await sut.On(envelope); |
||||
|
|
||||
|
A.CallTo(subscriptionService) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_publish_app_event() |
||||
|
{ |
||||
|
var envelope = Envelope.Create(new AppCreated()); |
||||
|
|
||||
|
await sut.On(envelope); |
||||
|
|
||||
|
A.CallTo(subscriptionService).Where(x => x.Method.Name.StartsWith("Publish")) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,202 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Reactive.Linq; |
||||
|
using FakeItEasy; |
||||
|
using GraphQL; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Subscriptions; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Squidex.Shared; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public class GraphQLSubscriptionTests : GraphQLTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task Should_subscribe_to_assets() |
||||
|
{ |
||||
|
var id = DomainId.NewGuid(); |
||||
|
|
||||
|
var query = CreateQuery(@"
|
||||
|
subscription { |
||||
|
assetChanges { |
||||
|
id, |
||||
|
fileName, |
||||
|
fileSize |
||||
|
} |
||||
|
}");
|
||||
|
|
||||
|
var stream = |
||||
|
Observable.Return<object>( |
||||
|
new EnrichedAssetEvent |
||||
|
{ |
||||
|
Id = id, |
||||
|
FileName = "image.png", |
||||
|
FileSize = 1024 |
||||
|
}); |
||||
|
|
||||
|
A.CallTo(() => subscriptionService.Subscribe<object>(A<AssetSubscription>._)) |
||||
|
.Returns(stream); |
||||
|
|
||||
|
var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, TestApp.Default.Name); |
||||
|
|
||||
|
var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
assetChanges = new |
||||
|
{ |
||||
|
id, |
||||
|
fileName = "image.png", |
||||
|
fileSize = 1024 |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertResult(expected, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_error_if_user_has_no_permissions_for_assets() |
||||
|
{ |
||||
|
var query = CreateQuery(@"
|
||||
|
subscription { |
||||
|
assetChanges { |
||||
|
id, |
||||
|
fileName, |
||||
|
fileSize |
||||
|
} |
||||
|
}");
|
||||
|
|
||||
|
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
errors = new[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
message = "You do not have the necessary permission.", |
||||
|
locations = new[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
line = 3, |
||||
|
column = 19 |
||||
|
} |
||||
|
}, |
||||
|
path = new[] |
||||
|
{ |
||||
|
"assetChanges" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data = (object?)null |
||||
|
}; |
||||
|
|
||||
|
AssertResult(expected, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_subscribe_to_contents() |
||||
|
{ |
||||
|
var id = DomainId.NewGuid(); |
||||
|
|
||||
|
var query = CreateQuery(@"
|
||||
|
subscription { |
||||
|
contentChanges { |
||||
|
id, |
||||
|
data |
||||
|
} |
||||
|
}");
|
||||
|
|
||||
|
var stream = |
||||
|
Observable.Return<object>( |
||||
|
new EnrichedContentEvent |
||||
|
{ |
||||
|
Id = id, |
||||
|
Data = new ContentData() |
||||
|
.AddField("field", |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant(42)) |
||||
|
}); |
||||
|
|
||||
|
A.CallTo(() => subscriptionService.Subscribe<object>(A<ContentSubscription>._)) |
||||
|
.Returns(stream); |
||||
|
|
||||
|
var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, TestApp.Default.Name, "random-schema"); |
||||
|
|
||||
|
var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
contentChanges = new |
||||
|
{ |
||||
|
id, |
||||
|
data = new |
||||
|
{ |
||||
|
field = new |
||||
|
{ |
||||
|
iv = 42 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertResult(expected, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_error_if_user_has_no_permissions_for_contents() |
||||
|
{ |
||||
|
var query = CreateQuery(@"
|
||||
|
subscription { |
||||
|
contentChanges { |
||||
|
id, |
||||
|
data |
||||
|
} |
||||
|
}");
|
||||
|
|
||||
|
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
errors = new[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
message = "You do not have the necessary permission.", |
||||
|
locations = new[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
line = 3, |
||||
|
column = 19 |
||||
|
} |
||||
|
}, |
||||
|
path = new[] |
||||
|
{ |
||||
|
"contentChanges" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data = (object?)null |
||||
|
}; |
||||
|
|
||||
|
AssertResult(expected, result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,46 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|
||||
import { inject, TestBed } from '@angular/core/testing'; |
|
||||
import { ApiUrlConfig, GraphQlService } from '@app/shared/internal'; |
|
||||
|
|
||||
describe('GraphQlService', () => { |
|
||||
beforeEach(() => { |
|
||||
TestBed.configureTestingModule({ |
|
||||
imports: [ |
|
||||
HttpClientTestingModule, |
|
||||
], |
|
||||
providers: [ |
|
||||
GraphQlService, |
|
||||
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, |
|
||||
], |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
|
||||
httpMock.verify(); |
|
||||
})); |
|
||||
|
|
||||
it('should make get request to get history events', |
|
||||
inject([GraphQlService, HttpTestingController], (graphQlService: GraphQlService, httpMock: HttpTestingController) => { |
|
||||
let graphQlResult: any = null; |
|
||||
|
|
||||
graphQlService.query('my-app', {}).subscribe(result => { |
|
||||
graphQlResult = result; |
|
||||
}); |
|
||||
|
|
||||
const req = httpMock.expectOne('http://service/p/api/content/my-app/graphql'); |
|
||||
|
|
||||
expect(req.request.method).toEqual('POST'); |
|
||||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|
||||
|
|
||||
req.flush({ result: true }); |
|
||||
|
|
||||
expect(graphQlResult).toEqual({ result: true }); |
|
||||
})); |
|
||||
}); |
|
||||
@ -1,26 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
import { HttpClient } from '@angular/common/http'; |
|
||||
import { Injectable } from '@angular/core'; |
|
||||
import { Observable } from 'rxjs'; |
|
||||
import { ApiUrlConfig } from '@app/framework'; |
|
||||
|
|
||||
@Injectable() |
|
||||
export class GraphQlService { |
|
||||
constructor( |
|
||||
private readonly http: HttpClient, |
|
||||
private readonly apiUrl: ApiUrlConfig, |
|
||||
) { |
|
||||
} |
|
||||
|
|
||||
public query(appName: string, params: any): Observable<any> { |
|
||||
const url = this.apiUrl.buildUrl(`api/content/${appName}/graphql`); |
|
||||
|
|
||||
return this.http.post(url, params); |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue