mirror of https://github.com/Squidex/squidex.git
50 changed files with 4944 additions and 9 deletions
@ -0,0 +1,30 @@ |
|||||
|
// ==========================================================================
|
||||
|
// CachingProviderBase.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities |
||||
|
{ |
||||
|
public abstract class CachingProviderBase |
||||
|
{ |
||||
|
private readonly IMemoryCache cache; |
||||
|
|
||||
|
protected IMemoryCache Cache |
||||
|
{ |
||||
|
get { return cache; } |
||||
|
} |
||||
|
|
||||
|
protected CachingProviderBase(IMemoryCache cache) |
||||
|
{ |
||||
|
Guard.NotNull(cache, nameof(cache)); |
||||
|
|
||||
|
this.cache = cache; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentHistoryEventsCreator.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.History; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase |
||||
|
{ |
||||
|
public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) |
||||
|
: base(typeNameRegistry) |
||||
|
{ |
||||
|
AddEventMessage<ContentCreated>( |
||||
|
"created content item."); |
||||
|
|
||||
|
AddEventMessage<ContentUpdated>( |
||||
|
"updated content item."); |
||||
|
|
||||
|
AddEventMessage<ContentDeleted>( |
||||
|
"deleted content item."); |
||||
|
|
||||
|
AddEventMessage<ContentStatusChanged>( |
||||
|
"changed status of content item to {[Status]}."); |
||||
|
} |
||||
|
|
||||
|
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
var channel = $"contents.{@event.Headers.AggregateId()}"; |
||||
|
|
||||
|
var result = ForEvent(@event.Payload, channel); |
||||
|
|
||||
|
if (@event.Payload is ContentStatusChanged contentStatusChanged) |
||||
|
{ |
||||
|
result = result.AddParameter("Status", contentStatusChanged.Status); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,216 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentQueryService.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.OData; |
||||
|
using Microsoft.OData.UriParser; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Scripting; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Edm; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class ContentQueryService : IContentQueryService |
||||
|
{ |
||||
|
private readonly IContentRepository contentRepository; |
||||
|
private readonly IAppProvider appProvider; |
||||
|
private readonly IScriptEngine scriptEngine; |
||||
|
private readonly EdmModelBuilder modelBuilder; |
||||
|
|
||||
|
public ContentQueryService( |
||||
|
IContentRepository contentRepository, |
||||
|
IAppProvider appProvider, |
||||
|
IScriptEngine scriptEngine, |
||||
|
EdmModelBuilder modelBuilder) |
||||
|
{ |
||||
|
Guard.NotNull(contentRepository, nameof(contentRepository)); |
||||
|
Guard.NotNull(scriptEngine, nameof(scriptEngine)); |
||||
|
Guard.NotNull(modelBuilder, nameof(modelBuilder)); |
||||
|
Guard.NotNull(appProvider, nameof(appProvider)); |
||||
|
|
||||
|
this.contentRepository = contentRepository; |
||||
|
this.appProvider = appProvider; |
||||
|
this.scriptEngine = scriptEngine; |
||||
|
this.modelBuilder = modelBuilder; |
||||
|
} |
||||
|
|
||||
|
public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id) |
||||
|
{ |
||||
|
Guard.NotNull(app, nameof(app)); |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); |
||||
|
|
||||
|
var isFrontendClient = user.IsInClient("squidex-frontend"); |
||||
|
|
||||
|
var schema = await FindSchemaAsync(app, schemaIdOrName); |
||||
|
|
||||
|
var content = await contentRepository.FindContentAsync(app, schema, id); |
||||
|
|
||||
|
if (content == null || (content.Status != Status.Published && !isFrontendClient)) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); |
||||
|
} |
||||
|
|
||||
|
content = TransformContent(user, schema, new List<IContentEntity> { content })[0]; |
||||
|
|
||||
|
return (schema, content); |
||||
|
} |
||||
|
|
||||
|
public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query) |
||||
|
{ |
||||
|
Guard.NotNull(app, nameof(app)); |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); |
||||
|
|
||||
|
var schema = await FindSchemaAsync(app, schemaIdOrName); |
||||
|
|
||||
|
var parsedQuery = ParseQuery(app, query, schema); |
||||
|
|
||||
|
var status = ParseStatus(user, archived); |
||||
|
|
||||
|
var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), parsedQuery); |
||||
|
var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), parsedQuery); |
||||
|
|
||||
|
await Task.WhenAll(taskForItems, taskForCount); |
||||
|
|
||||
|
var list = TransformContent(user, schema, taskForItems.Result.ToList()); |
||||
|
|
||||
|
return (schema, taskForCount.Result, list); |
||||
|
} |
||||
|
|
||||
|
public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(ids, nameof(ids)); |
||||
|
Guard.NotNull(app, nameof(app)); |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); |
||||
|
|
||||
|
var schema = await FindSchemaAsync(app, schemaIdOrName); |
||||
|
|
||||
|
var status = ParseStatus(user, archived); |
||||
|
|
||||
|
var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), ids); |
||||
|
var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), ids); |
||||
|
|
||||
|
await Task.WhenAll(taskForItems, taskForCount); |
||||
|
|
||||
|
var list = TransformContent(user, schema, taskForItems.Result.ToList()); |
||||
|
|
||||
|
return (schema, taskForCount.Result, list); |
||||
|
} |
||||
|
|
||||
|
private List<IContentEntity> TransformContent(ClaimsPrincipal user, ISchemaEntity schema, List<IContentEntity> contents) |
||||
|
{ |
||||
|
var scriptText = schema.ScriptQuery; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(scriptText)) |
||||
|
{ |
||||
|
for (var i = 0; i < contents.Count; i++) |
||||
|
{ |
||||
|
var content = contents[i]; |
||||
|
var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText); |
||||
|
|
||||
|
contents[i] = SimpleMapper.Map(content, new Content { Data = contentData }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return contents; |
||||
|
} |
||||
|
|
||||
|
private ODataUriParser ParseQuery(IAppEntity app, string query, ISchemaEntity schema) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var model = modelBuilder.BuildEdmModel(schema, app); |
||||
|
|
||||
|
return model.ParseQuery(query); |
||||
|
} |
||||
|
catch (ODataException ex) |
||||
|
{ |
||||
|
throw new ValidationException($"Failed to parse query: {ex.Message}", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<ISchemaEntity> FindSchemaAsync(IAppEntity app, string schemaIdOrName) |
||||
|
{ |
||||
|
Guard.NotNull(app, nameof(app)); |
||||
|
|
||||
|
ISchemaEntity schema = null; |
||||
|
|
||||
|
if (Guid.TryParse(schemaIdOrName, out var id)) |
||||
|
{ |
||||
|
schema = await appProvider.GetSchemaAsync(app.Name, id); |
||||
|
} |
||||
|
|
||||
|
if (schema == null) |
||||
|
{ |
||||
|
schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName); |
||||
|
} |
||||
|
|
||||
|
if (schema == null) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); |
||||
|
} |
||||
|
|
||||
|
return schema; |
||||
|
} |
||||
|
|
||||
|
private static List<Status> ParseStatus(ClaimsPrincipal user, bool archived) |
||||
|
{ |
||||
|
var status = new List<Status>(); |
||||
|
|
||||
|
if (user.IsInClient("squidex-frontend")) |
||||
|
{ |
||||
|
if (archived) |
||||
|
{ |
||||
|
status.Add(Status.Archived); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
status.Add(Status.Draft); |
||||
|
status.Add(Status.Published); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
status.Add(Status.Published); |
||||
|
} |
||||
|
|
||||
|
return status; |
||||
|
} |
||||
|
|
||||
|
private sealed class Content : IContentEntity |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
public Instant Created { get; set; } |
||||
|
public Instant LastModified { get; set; } |
||||
|
|
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
public NamedContentData Data { get; set; } |
||||
|
|
||||
|
public Status Status { get; set; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
// ==========================================================================
|
||||
|
// EdmModelBuilder.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.OData.Edm; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.GenerateEdmSchema; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Edm |
||||
|
{ |
||||
|
public class EdmModelBuilder : CachingProviderBase |
||||
|
{ |
||||
|
public EdmModelBuilder(IMemoryCache cache) |
||||
|
: base(cache) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public virtual IEdmModel BuildEdmModel(ISchemaEntity schema, IAppEntity app) |
||||
|
{ |
||||
|
Guard.NotNull(schema, nameof(schema)); |
||||
|
|
||||
|
var cacheKey = $"{schema.Id}_{schema.Version}_{app.Id}_{app.Version}"; |
||||
|
|
||||
|
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry => |
||||
|
{ |
||||
|
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); |
||||
|
|
||||
|
return BuildEdmModel(schema.SchemaDef, app.PartitionResolver()); |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static EdmModel BuildEdmModel(Schema schema, PartitionResolver partitionResolver) |
||||
|
{ |
||||
|
var model = new EdmModel(); |
||||
|
|
||||
|
var container = new EdmEntityContainer("Squidex", "Container"); |
||||
|
|
||||
|
var schemaType = schema.BuildEdmType(partitionResolver, x => |
||||
|
{ |
||||
|
model.AddElement(x); |
||||
|
|
||||
|
return x; |
||||
|
}); |
||||
|
|
||||
|
var entityType = new EdmEntityType("Squidex", schema.Name); |
||||
|
entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false)); |
||||
|
entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32); |
||||
|
entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); |
||||
|
entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String); |
||||
|
entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset); |
||||
|
entityType.AddStructuralProperty("lastModifiedBy", EdmPrimitiveTypeKind.String); |
||||
|
|
||||
|
model.AddElement(container); |
||||
|
model.AddElement(schemaType); |
||||
|
model.AddElement(entityType); |
||||
|
|
||||
|
container.AddEntitySet("ContentSet", entityType); |
||||
|
|
||||
|
return model; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
// ==========================================================================
|
||||
|
// EdmModelExtensions.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using Microsoft.OData.Edm; |
||||
|
using Microsoft.OData.UriParser; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Edm |
||||
|
{ |
||||
|
public static class EdmModelExtensions |
||||
|
{ |
||||
|
public static ODataUriParser ParseQuery(this IEdmModel model, string query) |
||||
|
{ |
||||
|
if (!model.EntityContainer.EntitySets().Any()) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
query = query ?? string.Empty; |
||||
|
|
||||
|
var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); |
||||
|
|
||||
|
if (query.StartsWith("?", StringComparison.Ordinal)) |
||||
|
{ |
||||
|
query = query.Substring(1); |
||||
|
} |
||||
|
|
||||
|
var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); |
||||
|
|
||||
|
return parser; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
// ==========================================================================
|
||||
|
// CachingGraphQLService.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService |
||||
|
{ |
||||
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); |
||||
|
private readonly IContentQueryService contentQuery; |
||||
|
private readonly IGraphQLUrlGenerator urlGenerator; |
||||
|
private readonly IAssetRepository assetRepository; |
||||
|
private readonly IAppProvider appProvider; |
||||
|
|
||||
|
public CachingGraphQLService(IMemoryCache cache, |
||||
|
IAppProvider appProvider, |
||||
|
IAssetRepository assetRepository, |
||||
|
IContentQueryService contentQuery, |
||||
|
IGraphQLUrlGenerator urlGenerator) |
||||
|
: base(cache) |
||||
|
{ |
||||
|
Guard.NotNull(appProvider, nameof(appProvider)); |
||||
|
Guard.NotNull(assetRepository, nameof(assetRepository)); |
||||
|
Guard.NotNull(contentQuery, nameof(urlGenerator)); |
||||
|
Guard.NotNull(contentQuery, nameof(contentQuery)); |
||||
|
|
||||
|
this.appProvider = appProvider; |
||||
|
this.assetRepository = assetRepository; |
||||
|
this.contentQuery = contentQuery; |
||||
|
this.urlGenerator = urlGenerator; |
||||
|
} |
||||
|
|
||||
|
public async Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query) |
||||
|
{ |
||||
|
Guard.NotNull(app, nameof(app)); |
||||
|
Guard.NotNull(query, nameof(query)); |
||||
|
|
||||
|
if (string.IsNullOrWhiteSpace(query.Query)) |
||||
|
{ |
||||
|
return (new object(), new object[0]); |
||||
|
} |
||||
|
|
||||
|
var modelContext = await GetModelAsync(app); |
||||
|
var queryContext = new GraphQLQueryContext(app, assetRepository, contentQuery, user, urlGenerator); |
||||
|
|
||||
|
return await modelContext.ExecuteAsync(queryContext, query); |
||||
|
} |
||||
|
|
||||
|
private async Task<GraphQLModel> GetModelAsync(IAppEntity app) |
||||
|
{ |
||||
|
var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); |
||||
|
|
||||
|
var modelContext = Cache.Get<GraphQLModel>(cacheKey); |
||||
|
|
||||
|
if (modelContext == null) |
||||
|
{ |
||||
|
var allSchemas = await appProvider.GetSchemasAsync(app.Name); |
||||
|
|
||||
|
modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator); |
||||
|
|
||||
|
Cache.Set(cacheKey, modelContext, CacheDuration); |
||||
|
} |
||||
|
|
||||
|
return modelContext; |
||||
|
} |
||||
|
|
||||
|
private static object CreateCacheKey(Guid appId, string etag) |
||||
|
{ |
||||
|
return $"GraphQLModel_{appId}_{etag}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,229 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphQLContext.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using GraphQL; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using GraphQLSchema = GraphQL.Types.Schema; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public sealed class GraphQLModel : IGraphQLContext |
||||
|
{ |
||||
|
private readonly Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> fieldInfos; |
||||
|
private readonly Dictionary<Guid, ContentGraphType> schemaTypes = new Dictionary<Guid, ContentGraphType>(); |
||||
|
private readonly Dictionary<Guid, ISchemaEntity> schemas; |
||||
|
private readonly PartitionResolver partitionResolver; |
||||
|
private readonly IAppEntity app; |
||||
|
private readonly IGraphType assetType; |
||||
|
private readonly IGraphType assetListType; |
||||
|
private readonly GraphQLSchema graphQLSchema; |
||||
|
|
||||
|
public bool CanGenerateAssetSourceUrl { get; } |
||||
|
|
||||
|
public GraphQLModel(IAppEntity app, IEnumerable<ISchemaEntity> schemas, IGraphQLUrlGenerator urlGenerator) |
||||
|
{ |
||||
|
this.app = app; |
||||
|
|
||||
|
CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; |
||||
|
|
||||
|
partitionResolver = app.PartitionResolver(); |
||||
|
|
||||
|
assetType = new AssetGraphType(this); |
||||
|
assetListType = new ListGraphType(new NonNullGraphType(assetType)); |
||||
|
|
||||
|
fieldInfos = new Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> |
||||
|
{ |
||||
|
{ |
||||
|
typeof(StringField), |
||||
|
field => ResolveDefault("String") |
||||
|
}, |
||||
|
{ |
||||
|
typeof(BooleanField), |
||||
|
field => ResolveDefault("Boolean") |
||||
|
}, |
||||
|
{ |
||||
|
typeof(NumberField), |
||||
|
field => ResolveDefault("Float") |
||||
|
}, |
||||
|
{ |
||||
|
typeof(DateTimeField), |
||||
|
field => ResolveDefault("Date") |
||||
|
}, |
||||
|
{ |
||||
|
typeof(JsonField), |
||||
|
field => ResolveDefault("Json") |
||||
|
}, |
||||
|
{ |
||||
|
typeof(TagsField), |
||||
|
field => ResolveDefault("String") |
||||
|
}, |
||||
|
{ |
||||
|
typeof(GeolocationField), |
||||
|
field => ResolveDefault("Geolocation") |
||||
|
}, |
||||
|
{ |
||||
|
typeof(AssetsField), |
||||
|
field => ResolveAssets(assetListType) |
||||
|
}, |
||||
|
{ |
||||
|
typeof(ReferencesField), |
||||
|
field => ResolveReferences(field) |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
this.schemas = schemas.ToDictionary(x => x.Id); |
||||
|
|
||||
|
graphQLSchema = new GraphQLSchema { Query = new ContentQueryGraphType(this, this.schemas.Values) }; |
||||
|
|
||||
|
foreach (var schemaType in schemaTypes.Values) |
||||
|
{ |
||||
|
schemaType.Initialize(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(string name) |
||||
|
{ |
||||
|
return (new NoopGraphType(name), new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName))); |
||||
|
} |
||||
|
|
||||
|
public IFieldResolver ResolveAssetUrl() |
||||
|
{ |
||||
|
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
|
||||
|
return context.UrlGenerator.GenerateAssetUrl(app, c.Source); |
||||
|
}); |
||||
|
|
||||
|
return resolver; |
||||
|
} |
||||
|
|
||||
|
public IFieldResolver ResolveAssetSourceUrl() |
||||
|
{ |
||||
|
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
|
||||
|
return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); |
||||
|
}); |
||||
|
|
||||
|
return resolver; |
||||
|
} |
||||
|
|
||||
|
public IFieldResolver ResolveAssetThumbnailUrl() |
||||
|
{ |
||||
|
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
|
||||
|
return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); |
||||
|
}); |
||||
|
|
||||
|
return resolver; |
||||
|
} |
||||
|
|
||||
|
public IFieldResolver ResolveContentUrl(ISchemaEntity schema) |
||||
|
{ |
||||
|
var resolver = new FuncFieldResolver<IContentEntity, object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
|
||||
|
return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); |
||||
|
}); |
||||
|
|
||||
|
return resolver; |
||||
|
} |
||||
|
|
||||
|
private static ValueTuple<IGraphType, IFieldResolver> ResolveAssets(IGraphType assetListType) |
||||
|
{ |
||||
|
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
var contentIds = c.Source.GetOrDefault(c.FieldName); |
||||
|
|
||||
|
return context.GetReferencedAssetsAsync(contentIds); |
||||
|
}); |
||||
|
|
||||
|
return (assetListType, resolver); |
||||
|
} |
||||
|
|
||||
|
private ValueTuple<IGraphType, IFieldResolver> ResolveReferences(Field field) |
||||
|
{ |
||||
|
var schemaId = ((ReferencesField)field).Properties.SchemaId; |
||||
|
var schemaType = GetSchemaType(schemaId); |
||||
|
|
||||
|
if (schemaType == null) |
||||
|
{ |
||||
|
return (null, null); |
||||
|
} |
||||
|
|
||||
|
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
var contentIds = c.Source.GetOrDefault(c.FieldName); |
||||
|
|
||||
|
return context.GetReferencedContentsAsync(schemaId, contentIds); |
||||
|
}); |
||||
|
|
||||
|
var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); |
||||
|
|
||||
|
return (schemaFieldType, resolver); |
||||
|
} |
||||
|
|
||||
|
public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLQueryContext context, GraphQLQuery query) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
|
||||
|
var result = await new DocumentExecuter().ExecuteAsync(options => |
||||
|
{ |
||||
|
options.Query = query.Query; |
||||
|
options.Schema = graphQLSchema; |
||||
|
options.Inputs = query.Variables?.ToInputs() ?? new Inputs(); |
||||
|
options.UserContext = context; |
||||
|
options.OperationName = query.OperationName; |
||||
|
}).ConfigureAwait(false); |
||||
|
|
||||
|
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); |
||||
|
} |
||||
|
|
||||
|
public IFieldPartitioning ResolvePartition(Partitioning key) |
||||
|
{ |
||||
|
return partitionResolver(key); |
||||
|
} |
||||
|
|
||||
|
public IGraphType GetAssetType() |
||||
|
{ |
||||
|
return assetType; |
||||
|
} |
||||
|
|
||||
|
public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) |
||||
|
{ |
||||
|
return fieldInfos[field.GetType()](field); |
||||
|
} |
||||
|
|
||||
|
public IGraphType GetSchemaType(Guid schemaId) |
||||
|
{ |
||||
|
var schema = schemas.GetOrDefault(schemaId); |
||||
|
|
||||
|
return schema != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schema, this)) : null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphQLQuery.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Newtonsoft.Json.Linq; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public class GraphQLQuery |
||||
|
{ |
||||
|
public string OperationName { get; set; } |
||||
|
|
||||
|
public string NamedQuery { get; set; } |
||||
|
|
||||
|
public string Query { get; set; } |
||||
|
|
||||
|
public JObject Variables { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphQLQueryContext.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public sealed class GraphQLQueryContext : QueryContext |
||||
|
{ |
||||
|
public IGraphQLUrlGenerator UrlGenerator { get; } |
||||
|
|
||||
|
public GraphQLQueryContext(IAppEntity app, IAssetRepository assetRepository, IContentQueryService contentQuery, ClaimsPrincipal user, |
||||
|
IGraphQLUrlGenerator urlGenerator) |
||||
|
: base(app, assetRepository, contentQuery, user) |
||||
|
{ |
||||
|
UrlGenerator = urlGenerator; |
||||
|
} |
||||
|
|
||||
|
public Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(JToken value) |
||||
|
{ |
||||
|
var ids = ParseIds(value); |
||||
|
|
||||
|
return GetReferencedAssetsAsync(ids); |
||||
|
} |
||||
|
|
||||
|
public Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, JToken value) |
||||
|
{ |
||||
|
var ids = ParseIds(value); |
||||
|
|
||||
|
return GetReferencedContentsAsync(schemaId, ids); |
||||
|
} |
||||
|
|
||||
|
private static ICollection<Guid> ParseIds(JToken value) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var result = new List<Guid>(); |
||||
|
|
||||
|
if (value is JArray) |
||||
|
{ |
||||
|
foreach (var id in value) |
||||
|
{ |
||||
|
result.Add(Guid.Parse(id.ToString())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return new List<Guid>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// ==========================================================================
|
||||
|
// SchemaGraphType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public interface IGraphQLContext |
||||
|
{ |
||||
|
bool CanGenerateAssetSourceUrl { get; } |
||||
|
|
||||
|
IFieldPartitioning ResolvePartition(Partitioning key); |
||||
|
|
||||
|
IGraphType GetAssetType(); |
||||
|
|
||||
|
IGraphType GetSchemaType(Guid schemaId); |
||||
|
|
||||
|
IFieldResolver ResolveAssetUrl(); |
||||
|
|
||||
|
IFieldResolver ResolveAssetSourceUrl(); |
||||
|
|
||||
|
IFieldResolver ResolveAssetThumbnailUrl(); |
||||
|
|
||||
|
IFieldResolver ResolveContentUrl(ISchemaEntity schema); |
||||
|
|
||||
|
(IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IGraphQLService.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public interface IGraphQLService |
||||
|
{ |
||||
|
Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IGraphQLUrlGenerator.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
||||
|
{ |
||||
|
public interface IGraphQLUrlGenerator |
||||
|
{ |
||||
|
bool CanGenerateAssetSourceUrl { get; } |
||||
|
|
||||
|
string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); |
||||
|
|
||||
|
string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); |
||||
|
|
||||
|
string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); |
||||
|
|
||||
|
string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,170 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AssetGraphType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity> |
||||
|
{ |
||||
|
public AssetGraphType(IGraphQLContext context) |
||||
|
{ |
||||
|
Name = "AssetDto"; |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "id", |
||||
|
Resolver = Resolver(x => x.Id.ToString()), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = "The id of the asset." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "version", |
||||
|
Resolver = Resolver(x => x.Version), |
||||
|
ResolvedType = new NonNullGraphType(new IntGraphType()), |
||||
|
Description = "The version of the asset." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "created", |
||||
|
Resolver = Resolver(x => x.Created.ToDateTimeUtc()), |
||||
|
ResolvedType = new NonNullGraphType(new DateGraphType()), |
||||
|
Description = "The date and time when the asset has been created." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "createdBy", |
||||
|
Resolver = Resolver(x => x.CreatedBy.ToString()), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = "The user that has created the asset." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModified", |
||||
|
Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), |
||||
|
ResolvedType = new NonNullGraphType(new DateGraphType()), |
||||
|
Description = "The date and time when the asset has been modified last." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModifiedBy", |
||||
|
Resolver = Resolver(x => x.LastModifiedBy.ToString()), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = "The user that has updated the asset last." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "mimeType", |
||||
|
Resolver = Resolver(x => x.MimeType), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = "The mime type." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "url", |
||||
|
Resolver = context.ResolveAssetUrl(), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = "The url to the asset." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "thumbnailUrl", |
||||
|
Resolver = context.ResolveAssetThumbnailUrl(), |
||||
|
ResolvedType = new StringGraphType(), |
||||
|
Description = "The thumbnail url to the asset." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileName", |
||||
|
Resolver = Resolver(x => x.FileName), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = "The file name." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileType", |
||||
|
Resolver = Resolver(x => x.FileName.FileType()), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = "The file type." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileSize", |
||||
|
Resolver = Resolver(x => x.FileSize), |
||||
|
ResolvedType = new NonNullGraphType(new IntGraphType()), |
||||
|
Description = "The size of the file in bytes." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "fileVersion", |
||||
|
Resolver = Resolver(x => x.FileVersion), |
||||
|
ResolvedType = new NonNullGraphType(new IntGraphType()), |
||||
|
Description = "The version of the file." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "isImage", |
||||
|
Resolver = Resolver(x => x.IsImage), |
||||
|
ResolvedType = new NonNullGraphType(new BooleanGraphType()), |
||||
|
Description = "Determines of the created file is an image." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "pixelWidth", |
||||
|
Resolver = Resolver(x => x.PixelWidth), |
||||
|
ResolvedType = new IntGraphType(), |
||||
|
Description = "The width of the image in pixels if the asset is an image." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "pixelHeight", |
||||
|
Resolver = Resolver(x => x.PixelHeight), |
||||
|
ResolvedType = new IntGraphType(), |
||||
|
Description = "The height of the image in pixels if the asset is an image." |
||||
|
}); |
||||
|
|
||||
|
if (context.CanGenerateAssetSourceUrl) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "sourceUrl", |
||||
|
Resolver = context.ResolveAssetSourceUrl(), |
||||
|
ResolvedType = new StringGraphType(), |
||||
|
Description = "The source url of the asset." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Description = "An asset"; |
||||
|
} |
||||
|
|
||||
|
private static IFieldResolver Resolver(Func<IAssetEntity, object> action) |
||||
|
{ |
||||
|
return new FuncFieldResolver<IAssetEntity, object>(c => action(c.Source)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentDataGraphType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> |
||||
|
{ |
||||
|
public ContentDataGraphType(Schema schema, IGraphQLContext context) |
||||
|
{ |
||||
|
var schemaName = schema.Properties.Label.WithFallback(schema.Name); |
||||
|
|
||||
|
Name = $"{schema.Name.ToPascalCase()}DataDto"; |
||||
|
|
||||
|
foreach (var field in schema.Fields.Where(x => !x.IsHidden)) |
||||
|
{ |
||||
|
var fieldInfo = context.GetGraphType(field); |
||||
|
|
||||
|
if (fieldInfo.ResolveType != null) |
||||
|
{ |
||||
|
var fieldName = field.RawProperties.Label.WithFallback(field.Name); |
||||
|
|
||||
|
var fieldGraphType = new ObjectGraphType |
||||
|
{ |
||||
|
Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" |
||||
|
}; |
||||
|
|
||||
|
var partition = context.ResolvePartition(field.Partitioning); |
||||
|
|
||||
|
foreach (var partitionItem in partition) |
||||
|
{ |
||||
|
fieldGraphType.AddField(new FieldType |
||||
|
{ |
||||
|
Name = partitionItem.Key, |
||||
|
Resolver = fieldInfo.Resolver, |
||||
|
ResolvedType = fieldInfo.ResolveType, |
||||
|
Description = field.RawProperties.Hints |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type."; |
||||
|
|
||||
|
var fieldResolver = new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(field.Name)); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = field.Name.ToCamelCase(), |
||||
|
Resolver = fieldResolver, |
||||
|
ResolvedType = fieldGraphType |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Description = $"The structure of a {schemaName} content type."; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
// ==========================================================================
|
||||
|
// SchemaGraphType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class ContentGraphType : ObjectGraphType<IContentEntity> |
||||
|
{ |
||||
|
private readonly ISchemaEntity schema; |
||||
|
private readonly IGraphQLContext context; |
||||
|
|
||||
|
public ContentGraphType(ISchemaEntity schema, IGraphQLContext context) |
||||
|
{ |
||||
|
this.context = context; |
||||
|
this.schema = schema; |
||||
|
|
||||
|
Name = $"{schema.Name.ToPascalCase()}Dto"; |
||||
|
} |
||||
|
|
||||
|
public void Initialize() |
||||
|
{ |
||||
|
var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.Name); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "id", |
||||
|
Resolver = Resolver(x => x.Id.ToString()), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = $"The id of the {schemaName} content." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "version", |
||||
|
Resolver = Resolver(x => x.Version), |
||||
|
ResolvedType = new NonNullGraphType(new IntGraphType()), |
||||
|
Description = $"The version of the {schemaName} content." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "created", |
||||
|
Resolver = Resolver(x => x.Created.ToDateTimeUtc()), |
||||
|
ResolvedType = new NonNullGraphType(new DateGraphType()), |
||||
|
Description = $"The date and time when the {schemaName} content has been created." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "createdBy", |
||||
|
Resolver = Resolver(x => x.CreatedBy.ToString()), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = $"The user that has created the {schemaName} content." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModified", |
||||
|
Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), |
||||
|
ResolvedType = new NonNullGraphType(new DateGraphType()), |
||||
|
Description = $"The date and time when the {schemaName} content has been modified last." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "lastModifiedBy", |
||||
|
Resolver = Resolver(x => x.LastModifiedBy.ToString()), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = $"The user that has updated the {schemaName} content last." |
||||
|
}); |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "url", |
||||
|
Resolver = context.ResolveContentUrl(schema), |
||||
|
ResolvedType = new NonNullGraphType(new StringGraphType()), |
||||
|
Description = $"The url to the the {schemaName} content." |
||||
|
}); |
||||
|
|
||||
|
var dataType = new ContentDataGraphType(schema.SchemaDef, context); |
||||
|
|
||||
|
if (dataType.Fields.Any()) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "data", |
||||
|
Resolver = Resolver(x => x.Data), |
||||
|
ResolvedType = new NonNullGraphType(dataType), |
||||
|
Description = $"The data of the {schemaName} content." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Description = $"The structure of a {schemaName} content type."; |
||||
|
} |
||||
|
|
||||
|
private static IFieldResolver Resolver(Func<IContentEntity, object> action) |
||||
|
{ |
||||
|
return new FuncFieldResolver<IContentEntity, object>(c => action(c.Source)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,192 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphModelType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class ContentQueryGraphType : ObjectGraphType |
||||
|
{ |
||||
|
public ContentQueryGraphType(IGraphQLContext graphQLContext, IEnumerable<ISchemaEntity> schemas) |
||||
|
{ |
||||
|
AddAssetFind(graphQLContext); |
||||
|
AddAssetsQuery(graphQLContext); |
||||
|
|
||||
|
foreach (var schema in schemas) |
||||
|
{ |
||||
|
var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name); |
||||
|
var schemaType = graphQLContext.GetSchemaType(schema.Id); |
||||
|
|
||||
|
AddContentFind(schema, schemaType, schemaName); |
||||
|
AddContentQuery(schema, schemaType, schemaName); |
||||
|
} |
||||
|
|
||||
|
Description = "The app queries."; |
||||
|
} |
||||
|
|
||||
|
private void AddAssetFind(IGraphQLContext graphQLContext) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "findAsset", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "id", |
||||
|
Description = "The id of the asset.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = graphQLContext.GetAssetType(), |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
||||
|
|
||||
|
return context.FindAssetAsync(contentId); |
||||
|
}), |
||||
|
Description = "Find an asset by id." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void AddContentFind(ISchemaEntity schema, IGraphType schemaType, string schemaName) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = $"find{schema.Name.ToPascalCase()}Content", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "id", |
||||
|
Description = $"The id of the {schemaName} content.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = schemaType, |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
||||
|
|
||||
|
return context.FindContentAsync(schema.Id, contentId); |
||||
|
}), |
||||
|
Description = $"Find an {schemaName} content by id." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void AddAssetsQuery(IGraphQLContext graphQLContext) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "queryAssets", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "top", |
||||
|
Description = "Optional number of assets to take.", |
||||
|
DefaultValue = 20 |
||||
|
}, |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "skip", |
||||
|
Description = "Optional number of assets to skip.", |
||||
|
DefaultValue = 0 |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "search", |
||||
|
Description = "Optional query.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = new ListGraphType(new NonNullGraphType(graphQLContext.GetAssetType())), |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
|
||||
|
var argTop = c.GetArgument("top", 20); |
||||
|
var argSkip = c.GetArgument("skip", 0); |
||||
|
var argQuery = c.GetArgument("search", string.Empty); |
||||
|
|
||||
|
return context.QueryAssetsAsync(argQuery, argSkip, argTop); |
||||
|
}), |
||||
|
Description = "Query assets items." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void AddContentQuery(ISchemaEntity schema, IGraphType schemaType, string schemaName) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = $"query{schema.Name.ToPascalCase()}Contents", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "top", |
||||
|
Description = "Optional number of contents to take.", |
||||
|
DefaultValue = 20 |
||||
|
}, |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "skip", |
||||
|
Description = "Optional number of contents to skip.", |
||||
|
DefaultValue = 0 |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "filter", |
||||
|
Description = "Optional OData filter.", |
||||
|
DefaultValue = string.Empty |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "search", |
||||
|
Description = "Optional OData full text search.", |
||||
|
DefaultValue = string.Empty |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "orderby", |
||||
|
Description = "Optional OData order definition.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (GraphQLQueryContext)c.UserContext; |
||||
|
var contentQuery = BuildODataQuery(c); |
||||
|
|
||||
|
return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); |
||||
|
}), |
||||
|
Description = $"Query {schemaName} content items." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private static string BuildODataQuery(ResolveFieldContext c) |
||||
|
{ |
||||
|
var odataQuery = "?" + |
||||
|
string.Join("&", |
||||
|
c.Arguments |
||||
|
.Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
||||
|
.Select(x => $"${x.Key}={x.Value}")); |
||||
|
|
||||
|
return odataQuery; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
// ==========================================================================
|
||||
|
// NoopGraphType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using GraphQL.Language.AST; |
||||
|
using GraphQL.Types; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class NoopGraphType : ScalarGraphType |
||||
|
{ |
||||
|
public NoopGraphType(string name) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
|
||||
|
public override object Serialize(object value) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
public override object ParseValue(object value) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
public override object ParseLiteral(IValue value) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IContentEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public interface IContentEntity : |
||||
|
IEntity, |
||||
|
IEntityWithAppRef, |
||||
|
IEntityWithCreatedBy, |
||||
|
IEntityWithLastModifiedBy, |
||||
|
IEntityWithVersion |
||||
|
{ |
||||
|
Status Status { get; } |
||||
|
|
||||
|
NamedContentData Data { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IContentQueryService.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public interface IContentQueryService |
||||
|
{ |
||||
|
Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids); |
||||
|
|
||||
|
Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); |
||||
|
|
||||
|
Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); |
||||
|
|
||||
|
Task<ISchemaEntity> FindSchemaAsync(IAppEntity app, string schemaIdOrName); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,146 @@ |
|||||
|
// ==========================================================================
|
||||
|
// QueryContext.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public class QueryContext |
||||
|
{ |
||||
|
private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>(); |
||||
|
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>(); |
||||
|
private readonly IContentQueryService contentQuery; |
||||
|
private readonly IAssetRepository assetRepository; |
||||
|
private readonly IAppEntity app; |
||||
|
private readonly ClaimsPrincipal user; |
||||
|
|
||||
|
public QueryContext( |
||||
|
IAppEntity app, |
||||
|
IAssetRepository assetRepository, |
||||
|
IContentQueryService contentQuery, |
||||
|
ClaimsPrincipal user) |
||||
|
{ |
||||
|
Guard.NotNull(assetRepository, nameof(assetRepository)); |
||||
|
Guard.NotNull(contentQuery, nameof(contentQuery)); |
||||
|
Guard.NotNull(app, nameof(app)); |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
|
||||
|
this.assetRepository = assetRepository; |
||||
|
this.contentQuery = contentQuery; |
||||
|
|
||||
|
this.user = user; |
||||
|
|
||||
|
this.app = app; |
||||
|
} |
||||
|
|
||||
|
public async Task<IAssetEntity> FindAssetAsync(Guid id) |
||||
|
{ |
||||
|
var asset = cachedAssets.GetOrDefault(id); |
||||
|
|
||||
|
if (asset == null) |
||||
|
{ |
||||
|
asset = await assetRepository.FindAssetAsync(id); |
||||
|
|
||||
|
if (asset != null) |
||||
|
{ |
||||
|
cachedAssets[asset.Id] = asset; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return asset; |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id) |
||||
|
{ |
||||
|
var content = cachedContents.GetOrDefault(id); |
||||
|
|
||||
|
if (content == null) |
||||
|
{ |
||||
|
content = (await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id)).Content; |
||||
|
|
||||
|
if (content != null) |
||||
|
{ |
||||
|
cachedContents[content.Id] = content; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IAssetEntity>> QueryAssetsAsync(string query, int skip = 0, int take = 10) |
||||
|
{ |
||||
|
var assets = await assetRepository.QueryAsync(app.Id, null, null, query, take, skip); |
||||
|
|
||||
|
foreach (var asset in assets) |
||||
|
{ |
||||
|
cachedAssets[asset.Id] = asset; |
||||
|
} |
||||
|
|
||||
|
return assets; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query) |
||||
|
{ |
||||
|
var contents = await contentQuery.QueryWithCountAsync(app, schemaIdOrName, user, false, query); |
||||
|
|
||||
|
foreach (var content in contents.Items) |
||||
|
{ |
||||
|
cachedContents[content.Id] = content; |
||||
|
} |
||||
|
|
||||
|
return contents.Items; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(ids, nameof(ids)); |
||||
|
|
||||
|
var notLoadedAssets = new HashSet<Guid>(ids.Where(id => !cachedAssets.ContainsKey(id))); |
||||
|
|
||||
|
if (notLoadedAssets.Count > 0) |
||||
|
{ |
||||
|
var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, null, int.MaxValue); |
||||
|
|
||||
|
foreach (var asset in assets) |
||||
|
{ |
||||
|
cachedAssets[asset.Id] = asset; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ids.Select(id => cachedAssets.GetOrDefault(id)).Where(x => x != null).ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(ids, nameof(ids)); |
||||
|
|
||||
|
var notLoadedContents = new HashSet<Guid>(ids.Where(id => !cachedContents.ContainsKey(id))); |
||||
|
|
||||
|
if (notLoadedContents.Count > 0) |
||||
|
{ |
||||
|
var contents = await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, notLoadedContents); |
||||
|
|
||||
|
foreach (var content in contents.Items) |
||||
|
{ |
||||
|
cachedContents[content.Id] = content; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ids.Select(id => cachedContents.GetOrDefault(id)).Where(x => x != null).ToList(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IContentRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.OData.UriParser; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Repositories |
||||
|
{ |
||||
|
public interface IContentRepository |
||||
|
{ |
||||
|
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids); |
||||
|
|
||||
|
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); |
||||
|
|
||||
|
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds); |
||||
|
|
||||
|
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids); |
||||
|
|
||||
|
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); |
||||
|
|
||||
|
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,244 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppCommandMiddlewareTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Services; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public class AppCommandMiddlewareTests : HandlerTestBase<AppDomainObject> |
||||
|
{ |
||||
|
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
||||
|
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>(); |
||||
|
private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>(); |
||||
|
private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); |
||||
|
private readonly AppCommandMiddleware sut; |
||||
|
private readonly AppDomainObject app; |
||||
|
private readonly Language language = Language.DE; |
||||
|
private readonly string contributorId = Guid.NewGuid().ToString(); |
||||
|
private readonly string clientName = "client"; |
||||
|
|
||||
|
public AppCommandMiddlewareTests() |
||||
|
{ |
||||
|
app = new AppDomainObject(); |
||||
|
|
||||
|
A.CallTo(() => appProvider.GetAppAsync(AppName)) |
||||
|
.Returns((IAppEntity)null); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdAsync(contributorId)) |
||||
|
.Returns(A.Fake<IUser>()); |
||||
|
|
||||
|
sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_create_domain_object() |
||||
|
{ |
||||
|
var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); |
||||
|
|
||||
|
await TestCreate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
|
||||
|
Assert.Equal(AppId, context.Result<EntityCreatedResult<Guid>>().IdOrValue); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task AssignContributor_should_update_domain_object_if_user_found() |
||||
|
{ |
||||
|
A.CallTo(() => appPlansProvider.GetPlan(null)) |
||||
|
.Returns(new ConfigAppLimitsPlan { MaxContributors = -1 }); |
||||
|
|
||||
|
CreateApp(); |
||||
|
|
||||
|
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RemoveContributor_should_update_domain_object() |
||||
|
{ |
||||
|
CreateApp() |
||||
|
.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); |
||||
|
|
||||
|
var context = CreateContextForCommand(new RemoveContributor { ContributorId = contributorId }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task AttachClient_should_update_domain_object() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
|
||||
|
var context = CreateContextForCommand(new AttachClient { Id = clientName }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RenameClient_should_update_domain_object() |
||||
|
{ |
||||
|
CreateApp() |
||||
|
.AttachClient(CreateCommand(new AttachClient { Id = clientName })); |
||||
|
|
||||
|
var context = CreateContextForCommand(new UpdateClient { Id = clientName, Name = "New Name" }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RevokeClient_should_update_domain_object() |
||||
|
{ |
||||
|
CreateApp() |
||||
|
.AttachClient(CreateCommand(new AttachClient { Id = clientName })); |
||||
|
|
||||
|
var context = CreateContextForCommand(new RevokeClient { Id = clientName }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ChangePlan_should_update_domain_object() |
||||
|
{ |
||||
|
A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) |
||||
|
.Returns(true); |
||||
|
|
||||
|
CreateApp(); |
||||
|
|
||||
|
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
|
||||
|
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ChangePlan_should_not_make_update_for_redirect_result() |
||||
|
{ |
||||
|
A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) |
||||
|
.Returns(true); |
||||
|
|
||||
|
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) |
||||
|
.Returns(CreateRedirectResult()); |
||||
|
|
||||
|
CreateApp(); |
||||
|
|
||||
|
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
|
||||
|
Assert.Null(app.State.Plan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ChangePlan_should_not_call_billing_manager_for_callback() |
||||
|
{ |
||||
|
A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) |
||||
|
.Returns(true); |
||||
|
|
||||
|
CreateApp(); |
||||
|
|
||||
|
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan", FromCallback = true }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
|
||||
|
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task AddLanguage_should_update_domain_object() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
|
||||
|
var context = CreateContextForCommand(new AddLanguage { Language = language }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RemoveLanguage_should_update_domain_object() |
||||
|
{ |
||||
|
CreateApp() |
||||
|
.AddLanguage(CreateCommand(new AddLanguage { Language = language })); |
||||
|
|
||||
|
var context = CreateContextForCommand(new RemoveLanguage { Language = language }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task UpdateLanguage_should_update_domain_object() |
||||
|
{ |
||||
|
CreateApp() |
||||
|
.AddLanguage(CreateCommand(new AddLanguage { Language = language })); |
||||
|
|
||||
|
var context = CreateContextForCommand(new UpdateLanguage { Language = language }); |
||||
|
|
||||
|
await TestUpdate(app, async _ => |
||||
|
{ |
||||
|
await sut.HandleAsync(context); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private AppDomainObject CreateApp() |
||||
|
{ |
||||
|
app.Create(CreateCommand(new CreateApp { AppId = AppId, Name = AppName })); |
||||
|
|
||||
|
return app; |
||||
|
} |
||||
|
|
||||
|
private static Task<IChangePlanResult> CreateRedirectResult() |
||||
|
{ |
||||
|
return Task.FromResult<IChangePlanResult>(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,284 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppDomainObjectTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public class AppDomainObjectTests : HandlerTestBase<AppDomainObject> |
||||
|
{ |
||||
|
private readonly AppDomainObject sut; |
||||
|
private readonly string contributorId = Guid.NewGuid().ToString(); |
||||
|
private readonly string clientId = "client"; |
||||
|
private readonly string clientNewName = "My Client"; |
||||
|
private readonly string planId = "premium"; |
||||
|
|
||||
|
public AppDomainObjectTests() |
||||
|
{ |
||||
|
sut = new AppDomainObject(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Create_should_throw_exception_if_created() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
|
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.Create(CreateCommand(new CreateApp { Name = AppName })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Create_should_specify_name_and_owner() |
||||
|
{ |
||||
|
sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId })); |
||||
|
|
||||
|
Assert.Equal(AppName, sut.State.Name); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppCreated { Name = AppName }), |
||||
|
CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), |
||||
|
CreateEvent(new AppLanguageAdded { Language = Language.EN }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ChangePlan_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ChangePlan_should_create_events() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
|
||||
|
sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppPlanChanged { PlanId = planId }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AssignContributor_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AssignContributor_should_create_events() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
|
||||
|
sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RemoveContributor_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RemoveContributor_should_create_events_and_remove_contributor() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
|
||||
|
sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); |
||||
|
sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); |
||||
|
|
||||
|
sut.GetUncomittedEvents().Skip(1) |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AttachClient_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AttachClient_should_create_events() |
||||
|
{ |
||||
|
var command = new AttachClient { Id = clientId }; |
||||
|
|
||||
|
CreateApp(); |
||||
|
|
||||
|
sut.AttachClient(CreateCommand(command)); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RevokeClient_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RevokeClient_should_create_events() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
CreateClient(); |
||||
|
|
||||
|
sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppClientRevoked { Id = clientId }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_create_events() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
CreateClient(); |
||||
|
|
||||
|
sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), |
||||
|
CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AddLanguage_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AddLanguage_should_create_events() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
|
||||
|
sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppLanguageAdded { Language = Language.DE }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RemoveLanguage_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RemoveLanguage_should_create_events() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
CreateLanguage(Language.DE); |
||||
|
|
||||
|
sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppLanguageRemoved { Language = Language.DE }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateLanguage_should_throw_exception_if_not_created() |
||||
|
{ |
||||
|
Assert.Throws<DomainException>(() => |
||||
|
{ |
||||
|
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN })); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateLanguage_should_create_events() |
||||
|
{ |
||||
|
CreateApp(); |
||||
|
CreateLanguage(Language.DE); |
||||
|
|
||||
|
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } })); |
||||
|
|
||||
|
sut.GetUncomittedEvents() |
||||
|
.ShouldHaveSameEvents( |
||||
|
CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List<Language> { Language.EN } }) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private void CreateApp() |
||||
|
{ |
||||
|
sut.Create(CreateCommand(new CreateApp { Name = AppName })); |
||||
|
sut.ClearUncommittedEvents(); |
||||
|
} |
||||
|
|
||||
|
private void CreateClient() |
||||
|
{ |
||||
|
sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); |
||||
|
sut.ClearUncommittedEvents(); |
||||
|
} |
||||
|
|
||||
|
private void CreateLanguage(Language language) |
||||
|
{ |
||||
|
sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); |
||||
|
sut.ClearUncommittedEvents(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppEventTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Domain.Apps.Events.Apps.Old; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public class AppEventTests |
||||
|
{ |
||||
|
private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); |
||||
|
private readonly NamedId<Guid> appId = new NamedId<Guid>(Guid.NewGuid(), "my-app"); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_migrate_client_changed_as_reader_to_client_updated() |
||||
|
{ |
||||
|
var source = CreateEvent(new AppClientChanged { IsReader = true }); |
||||
|
|
||||
|
source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Reader })); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_migrate_client_changed_as_writer_to_client_updated() |
||||
|
{ |
||||
|
var source = CreateEvent(new AppClientChanged { IsReader = false }); |
||||
|
|
||||
|
source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Editor })); |
||||
|
} |
||||
|
|
||||
|
private T CreateEvent<T>(T contentEvent) where T : AppEvent |
||||
|
{ |
||||
|
contentEvent.Actor = actor; |
||||
|
contentEvent.AppId = appId; |
||||
|
|
||||
|
return contentEvent; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,159 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ConfigAppLimitsProviderTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
using FakeItEasy; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public class ConfigAppLimitsProviderTests |
||||
|
{ |
||||
|
private static readonly ConfigAppLimitsPlan InfinitePlan = new ConfigAppLimitsPlan |
||||
|
{ |
||||
|
Id = "infinite", |
||||
|
Name = "Infinite", |
||||
|
MaxApiCalls = -1, |
||||
|
MaxAssetSize = -1, |
||||
|
MaxContributors = -1 |
||||
|
}; |
||||
|
|
||||
|
private static readonly ConfigAppLimitsPlan FreePlan = new ConfigAppLimitsPlan |
||||
|
{ |
||||
|
Id = "free", |
||||
|
Name = "Free", |
||||
|
MaxApiCalls = 50000, |
||||
|
MaxAssetSize = 1024 * 1024 * 10, |
||||
|
MaxContributors = 2 |
||||
|
}; |
||||
|
|
||||
|
private static readonly ConfigAppLimitsPlan BasicPlan = new ConfigAppLimitsPlan |
||||
|
{ |
||||
|
Id = "basic", |
||||
|
Name = "Basic", |
||||
|
MaxApiCalls = 150000, |
||||
|
MaxAssetSize = 1024 * 1024 * 2, |
||||
|
MaxContributors = 5 |
||||
|
}; |
||||
|
|
||||
|
private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_plans() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
Plans.OrderBy(x => x.MaxApiCalls).ShouldBeEquivalentTo(sut.GetAvailablePlans()); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(null)] |
||||
|
[InlineData("my-plan")] |
||||
|
public void Should_return_infinite_if_nothing_configured(string planId) |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Enumerable.Empty<ConfigAppLimitsPlan>()); |
||||
|
|
||||
|
var plan = sut.GetPlanForApp(CreateApp(planId)); |
||||
|
|
||||
|
plan.ShouldBeEquivalentTo(InfinitePlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_fitting_app_plan() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
var plan = sut.GetPlanForApp(CreateApp("basic")); |
||||
|
|
||||
|
plan.ShouldBeEquivalentTo(BasicPlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_smallest_plan_if_none_fits() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
var plan = sut.GetPlanForApp(CreateApp("enterprise")); |
||||
|
|
||||
|
plan.ShouldBeEquivalentTo(FreePlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_second_plan_for_upgrade_if_plan_is_null() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
var upgradePlan = sut.GetPlanUpgrade(null); |
||||
|
|
||||
|
upgradePlan.ShouldBeEquivalentTo(BasicPlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_second_plan_for_upgrade_if_plan_not_found() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("enterprise")); |
||||
|
|
||||
|
upgradePlan.ShouldBeEquivalentTo(BasicPlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_return_plan_for_upgrade_if_plan_is_highest_plan() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("basic")); |
||||
|
|
||||
|
Assert.Null(upgradePlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_next_plan_if_plan_is_upgradeable() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("free")); |
||||
|
|
||||
|
upgradePlan.ShouldBeEquivalentTo(BasicPlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_check_plan_exists() |
||||
|
{ |
||||
|
var sut = new ConfigAppPlansProvider(Plans); |
||||
|
|
||||
|
Assert.True(sut.IsConfiguredPlan("basic")); |
||||
|
Assert.True(sut.IsConfiguredPlan("free")); |
||||
|
|
||||
|
Assert.False(sut.IsConfiguredPlan("infinite")); |
||||
|
Assert.False(sut.IsConfiguredPlan("invalid")); |
||||
|
Assert.False(sut.IsConfiguredPlan(null)); |
||||
|
} |
||||
|
|
||||
|
private static IAppEntity CreateApp(string plan) |
||||
|
{ |
||||
|
var app = A.Dummy<IAppEntity>(); |
||||
|
|
||||
|
if (plan != null) |
||||
|
{ |
||||
|
A.CallTo(() => app.Plan).Returns(new AppPlan(new RefToken("user", "me"), plan)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
A.CallTo(() => app.Plan).Returns(null); |
||||
|
} |
||||
|
|
||||
|
return app; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,142 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GuardAppClientsTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable SA1310 // Field names must not contain underscore
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
||||
|
{ |
||||
|
public class GuardAppClientsTests |
||||
|
{ |
||||
|
private readonly AppClients clients_0 = AppClients.Empty; |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanAttach_should_throw_execption_if_client_id_is_null() |
||||
|
{ |
||||
|
var command = new AttachClient(); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanAttach(clients_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanAttach_should_throw_exception_if_client_already_exists() |
||||
|
{ |
||||
|
var command = new AttachClient { Id = "android" }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("android", "secret"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanAttach(clients_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanAttach_should_not_throw_exception_if_client_is_free() |
||||
|
{ |
||||
|
var command = new AttachClient { Id = "ios" }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("android", "secret"); |
||||
|
|
||||
|
GuardAppClients.CanAttach(clients_1, command); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRevoke_should_throw_execption_if_client_id_is_null() |
||||
|
{ |
||||
|
var command = new RevokeClient(); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanRevoke(clients_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRevoke_should_throw_exception_if_client_is_not_found() |
||||
|
{ |
||||
|
var command = new RevokeClient { Id = "ios" }; |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanRevoke(clients_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRevoke_should_not_throw_exception_if_client_is_found() |
||||
|
{ |
||||
|
var command = new RevokeClient { Id = "ios" }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("ios", "secret"); |
||||
|
|
||||
|
GuardAppClients.CanRevoke(clients_1, command); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdate_should_throw_execption_if_client_id_is_null() |
||||
|
{ |
||||
|
var command = new UpdateClient(); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_throw_exception_if_client_is_not_found() |
||||
|
{ |
||||
|
var command = new UpdateClient { Id = "ios", Name = "iOS" }; |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanUpdate(clients_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_throw_exception_if_client_has_no_name_and_permission() |
||||
|
{ |
||||
|
var command = new UpdateClient { Id = "ios" }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("ios", "secret"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_throw_exception_if_client_has_invalid_permission() |
||||
|
{ |
||||
|
var command = new UpdateClient { Id = "ios", Permission = (AppClientPermission)10 }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("ios", "secret"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_throw_exception_if_client_has_same_name() |
||||
|
{ |
||||
|
var command = new UpdateClient { Id = "ios", Name = "ios" }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("ios", "secret"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_throw_exception_if_client_has_same_permission() |
||||
|
{ |
||||
|
var command = new UpdateClient { Id = "ios", Permission = AppClientPermission.Editor }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("ios", "secret"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void UpdateClient_should_not_throw_exception_if_command_is_valid() |
||||
|
{ |
||||
|
var command = new UpdateClient { Id = "ios", Name = "iOS", Permission = AppClientPermission.Reader }; |
||||
|
|
||||
|
var clients_1 = clients_0.Add("ios", "secret"); |
||||
|
|
||||
|
GuardAppClients.CanUpdate(clients_1, command); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,158 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GuardAppContributorsTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Services; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable SA1310 // Field names must not contain underscore
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
||||
|
{ |
||||
|
public class GuardAppContributorsTests |
||||
|
{ |
||||
|
private readonly IUserResolver users = A.Fake<IUserResolver>(); |
||||
|
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>(); |
||||
|
private readonly AppContributors contributors_0 = AppContributors.Empty; |
||||
|
|
||||
|
public GuardAppContributorsTests() |
||||
|
{ |
||||
|
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) |
||||
|
.Returns(A.Fake<IUser>()); |
||||
|
|
||||
|
A.CallTo(() => appPlan.MaxContributors) |
||||
|
.Returns(10); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_throw_exception_if_contributor_id_is_null() |
||||
|
{ |
||||
|
var command = new AssignContributor(); |
||||
|
|
||||
|
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_throw_exception_if_permission_not_valid() |
||||
|
{ |
||||
|
var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; |
||||
|
|
||||
|
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_throw_exception_if_user_already_exists_with_same_permission() |
||||
|
{ |
||||
|
var command = new AssignContributor { ContributorId = "1" }; |
||||
|
|
||||
|
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
||||
|
|
||||
|
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_throw_exception_if_user_not_found() |
||||
|
{ |
||||
|
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) |
||||
|
.Returns(Task.FromResult<IUser>(null)); |
||||
|
|
||||
|
var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; |
||||
|
|
||||
|
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_throw_exception_if_contributor_max_reached() |
||||
|
{ |
||||
|
A.CallTo(() => appPlan.MaxContributors) |
||||
|
.Returns(2); |
||||
|
|
||||
|
var command = new AssignContributor { ContributorId = "3" }; |
||||
|
|
||||
|
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
||||
|
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); |
||||
|
|
||||
|
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_not_throw_exception_if_user_found() |
||||
|
{ |
||||
|
var command = new AssignContributor { ContributorId = "1" }; |
||||
|
|
||||
|
return GuardAppContributors.CanAssign(contributors_0, command, users, appPlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_not_throw_exception_if_contributor_has_another_permission() |
||||
|
{ |
||||
|
var command = new AssignContributor { ContributorId = "1" }; |
||||
|
|
||||
|
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); |
||||
|
|
||||
|
return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_permission_changed() |
||||
|
{ |
||||
|
A.CallTo(() => appPlan.MaxContributors) |
||||
|
.Returns(2); |
||||
|
|
||||
|
var command = new AssignContributor { ContributorId = "1" }; |
||||
|
|
||||
|
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); |
||||
|
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); |
||||
|
|
||||
|
return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemove_should_throw_exception_if_contributor_id_is_null() |
||||
|
{ |
||||
|
var command = new RemoveContributor(); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppContributors.CanRemove(contributors_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemove_should_throw_exception_if_contributor_not_found() |
||||
|
{ |
||||
|
var command = new RemoveContributor { ContributorId = "1" }; |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppContributors.CanRemove(contributors_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemove_should_throw_exception_if_contributor_is_only_owner() |
||||
|
{ |
||||
|
var command = new RemoveContributor { ContributorId = "1" }; |
||||
|
|
||||
|
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
||||
|
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppContributors.CanRemove(contributors_2, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() |
||||
|
{ |
||||
|
var command = new RemoveContributor { ContributorId = "1" }; |
||||
|
|
||||
|
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
||||
|
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Owner); |
||||
|
|
||||
|
GuardAppContributors.CanRemove(contributors_2, command); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GuardAppLanguagesTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable SA1310 // Field names must not contain underscore
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
||||
|
{ |
||||
|
public class GuardAppLanguagesTests |
||||
|
{ |
||||
|
private readonly LanguagesConfig languages_0 = LanguagesConfig.Build(Language.DE); |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanAddLanguage_should_throw_exception_if_language_is_null() |
||||
|
{ |
||||
|
var command = new AddLanguage(); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanAdd(languages_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanAddLanguage_should_throw_exception_if_language_already_added() |
||||
|
{ |
||||
|
var command = new AddLanguage { Language = Language.DE }; |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanAdd(languages_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanAddLanguage_should_not_throw_exception_if_language_valid() |
||||
|
{ |
||||
|
var command = new AddLanguage { Language = Language.EN }; |
||||
|
|
||||
|
GuardAppLanguages.CanAdd(languages_0, command); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemoveLanguage_should_throw_exception_if_language_is_null() |
||||
|
{ |
||||
|
var command = new RemoveLanguage(); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanRemove(languages_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemoveLanguage_should_throw_exception_if_language_not_found() |
||||
|
{ |
||||
|
var command = new RemoveLanguage { Language = Language.EN }; |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanRemove(languages_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemoveLanguage_should_throw_exception_if_language_is_master() |
||||
|
{ |
||||
|
var command = new RemoveLanguage { Language = Language.DE }; |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanRemove(languages_0, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid() |
||||
|
{ |
||||
|
var command = new RemoveLanguage { Language = Language.EN }; |
||||
|
|
||||
|
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
||||
|
|
||||
|
GuardAppLanguages.CanRemove(languages_1, command); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdateLanguage_should_throw_exception_if_language_is_null() |
||||
|
{ |
||||
|
var command = new UpdateLanguage(); |
||||
|
|
||||
|
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() |
||||
|
{ |
||||
|
var command = new UpdateLanguage { Language = Language.DE, IsOptional = true }; |
||||
|
|
||||
|
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() |
||||
|
{ |
||||
|
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.IT } }; |
||||
|
|
||||
|
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdateLanguage_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var command = new UpdateLanguage { Language = Language.IT }; |
||||
|
|
||||
|
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() |
||||
|
{ |
||||
|
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } }; |
||||
|
|
||||
|
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
||||
|
|
||||
|
GuardAppLanguages.CanUpdate(languages_1, command); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GuardAppTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Services; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
||||
|
{ |
||||
|
public class GuardAppTests |
||||
|
{ |
||||
|
private readonly IAppProvider apps = A.Fake<IAppProvider>(); |
||||
|
private readonly IUserResolver users = A.Fake<IUserResolver>(); |
||||
|
private readonly IAppPlansProvider appPlans = A.Fake<IAppPlansProvider>(); |
||||
|
|
||||
|
public GuardAppTests() |
||||
|
{ |
||||
|
A.CallTo(() => apps.GetAppAsync("new-app")) |
||||
|
.Returns(Task.FromResult<IAppEntity>(null)); |
||||
|
|
||||
|
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) |
||||
|
.Returns(A.Fake<IUser>()); |
||||
|
|
||||
|
A.CallTo(() => appPlans.GetPlan("free")) |
||||
|
.Returns(A.Fake<IAppLimitsPlan>()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanCreate_should_throw_exception_if_name_already_in_use() |
||||
|
{ |
||||
|
A.CallTo(() => apps.GetAppAsync("new-app")) |
||||
|
.Returns(A.Fake<IAppEntity>()); |
||||
|
|
||||
|
var command = new CreateApp { Name = "new-app" }; |
||||
|
|
||||
|
return Assert.ThrowsAsync<ValidationException>(() => GuardApp.CanCreate(command, apps)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanCreate_should_throw_exception_if_name_not_valid() |
||||
|
{ |
||||
|
var command = new CreateApp { Name = "INVALID NAME" }; |
||||
|
|
||||
|
return Assert.ThrowsAsync<ValidationException>(() => GuardApp.CanCreate(command, apps)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task CanCreate_should_not_throw_exception_if_app_name_is_free() |
||||
|
{ |
||||
|
var command = new CreateApp { Name = "new-app" }; |
||||
|
|
||||
|
return GuardApp.CanCreate(command, apps); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanChangePlan_should_throw_exception_if_plan_id_null() |
||||
|
{ |
||||
|
var command = new ChangePlan { Actor = new RefToken("user", "me") }; |
||||
|
|
||||
|
AppPlan plan = null; |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanChangePlan_should_throw_exception_if_plan_not_found() |
||||
|
{ |
||||
|
A.CallTo(() => appPlans.GetPlan("free")) |
||||
|
.Returns(null); |
||||
|
|
||||
|
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
||||
|
|
||||
|
AppPlan plan = null; |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() |
||||
|
{ |
||||
|
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
||||
|
|
||||
|
var plan = new AppPlan(new RefToken("user", "other"), "premium"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanChangePlan_should_throw_exception_if_plan_is_the_same() |
||||
|
{ |
||||
|
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
||||
|
|
||||
|
var plan = new AppPlan(new RefToken("user", "me"), "free"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() |
||||
|
{ |
||||
|
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
||||
|
|
||||
|
var plan = new AppPlan(new RefToken("user", "me"), "premium"); |
||||
|
|
||||
|
GuardApp.CanChangePlan(command, plan, appPlans); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// ==========================================================================
|
||||
|
// NoopAppPlanBillingManagerTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public class NoopAppPlanBillingManagerTests |
||||
|
{ |
||||
|
private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_have_portal() |
||||
|
{ |
||||
|
Assert.False(sut.HasPortal); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_do_nothing_when_changing_plan() |
||||
|
{ |
||||
|
await sut.ChangePlanAsync(null, Guid.Empty, null, null); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_return_portal_link() |
||||
|
{ |
||||
|
Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,220 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentQueryServiceTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.OData.UriParser; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Scripting; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Edm; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public class ContentQueryServiceTests |
||||
|
{ |
||||
|
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); |
||||
|
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); |
||||
|
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>(); |
||||
|
private readonly IContentEntity content = A.Fake<IContentEntity>(); |
||||
|
private readonly IAppEntity app = A.Fake<IAppEntity>(); |
||||
|
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly Guid schemaId = Guid.NewGuid(); |
||||
|
private readonly Guid contentId = Guid.NewGuid(); |
||||
|
private readonly string appName = "my-app"; |
||||
|
private readonly NamedContentData data = new NamedContentData(); |
||||
|
private readonly NamedContentData transformedData = new NamedContentData(); |
||||
|
private readonly ClaimsPrincipal user; |
||||
|
private readonly ClaimsIdentity identity = new ClaimsIdentity(); |
||||
|
private readonly EdmModelBuilder modelBuilder = A.Fake<EdmModelBuilder>(); |
||||
|
private readonly ContentQueryService sut; |
||||
|
|
||||
|
public ContentQueryServiceTests() |
||||
|
{ |
||||
|
user = new ClaimsPrincipal(identity); |
||||
|
|
||||
|
A.CallTo(() => app.Id).Returns(appId); |
||||
|
A.CallTo(() => app.Name).Returns(appName); |
||||
|
|
||||
|
A.CallTo(() => content.Id).Returns(contentId); |
||||
|
A.CallTo(() => content.Data).Returns(data); |
||||
|
A.CallTo(() => content.Status).Returns(Status.Published); |
||||
|
|
||||
|
sut = new ContentQueryService(contentRepository, appProvider, scriptEngine, modelBuilder); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_schema_from_id_if_string_is_guid() |
||||
|
{ |
||||
|
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
||||
|
.Returns(schema); |
||||
|
|
||||
|
var result = await sut.FindSchemaAsync(app, schemaId.ToString()); |
||||
|
|
||||
|
Assert.Equal(schema, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_schema_from_name_if_string_not_guid() |
||||
|
{ |
||||
|
A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) |
||||
|
.Returns(schema); |
||||
|
|
||||
|
var result = await sut.FindSchemaAsync(app, "my-schema"); |
||||
|
|
||||
|
Assert.Equal(schema, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_if_schema_not_found() |
||||
|
{ |
||||
|
A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) |
||||
|
.Returns((ISchemaEntity)null); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.FindSchemaAsync(app, "my-schema")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_content_from_repository_and_transform() |
||||
|
{ |
||||
|
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
||||
|
.Returns(schema); |
||||
|
A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) |
||||
|
.Returns(content); |
||||
|
|
||||
|
A.CallTo(() => schema.ScriptQuery) |
||||
|
.Returns("<script-query>"); |
||||
|
|
||||
|
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "<query-script>")) |
||||
|
.Returns(transformedData); |
||||
|
|
||||
|
var result = await sut.FindContentAsync(app, schemaId.ToString(), user, contentId); |
||||
|
|
||||
|
Assert.Equal(schema, result.Schema); |
||||
|
Assert.Equal(data, result.Content.Data); |
||||
|
Assert.Equal(content.Id, result.Content.Id); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_if_content_to_find_does_not_exist() |
||||
|
{ |
||||
|
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
||||
|
.Returns(schema); |
||||
|
A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) |
||||
|
.Returns((IContentEntity)null); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(async () => await sut.FindContentAsync(app, schemaId.ToString(), user, contentId)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_contents_with_ids_from_repository_and_transform() |
||||
|
{ |
||||
|
await TestManyIdRequest(true, false, new HashSet<Guid> { Guid.NewGuid() }, Status.Draft, Status.Published); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_non_archived_contents_from_repository_and_transform() |
||||
|
{ |
||||
|
await TestManyRequest(true, false, Status.Draft, Status.Published); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_archived_contents_from_repository_and_transform() |
||||
|
{ |
||||
|
await TestManyRequest(true, true, Status.Archived); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_draft_contents_from_repository_and_transform() |
||||
|
{ |
||||
|
await TestManyRequest(false, false, Status.Published); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_draft_contents_from_repository_and_transform_when_requesting_archive_as_non_frontend() |
||||
|
{ |
||||
|
await TestManyRequest(false, true, Status.Published); |
||||
|
} |
||||
|
|
||||
|
private async Task TestManyRequest(bool isFrontend, bool archive, params Status[] status) |
||||
|
{ |
||||
|
SetupClaims(isFrontend); |
||||
|
|
||||
|
SetupFakeWithOdataQuery(status); |
||||
|
SetupFakeWithScripting(); |
||||
|
|
||||
|
var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, string.Empty); |
||||
|
|
||||
|
Assert.Equal(123, result.Total); |
||||
|
Assert.Equal(schema, result.Schema); |
||||
|
Assert.Equal(data, result.Items[0].Data); |
||||
|
Assert.Equal(content.Id, result.Items[0].Id); |
||||
|
} |
||||
|
|
||||
|
private async Task TestManyIdRequest(bool isFrontend, bool archive, HashSet<Guid> ids, params Status[] status) |
||||
|
{ |
||||
|
SetupClaims(isFrontend); |
||||
|
|
||||
|
SetupFakeWithIdQuery(status, ids); |
||||
|
SetupFakeWithScripting(); |
||||
|
|
||||
|
var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, ids); |
||||
|
|
||||
|
Assert.Equal(123, result.Total); |
||||
|
Assert.Equal(schema, result.Schema); |
||||
|
Assert.Equal(data, result.Items[0].Data); |
||||
|
Assert.Equal(content.Id, result.Items[0].Id); |
||||
|
} |
||||
|
|
||||
|
private void SetupClaims(bool isFrontend) |
||||
|
{ |
||||
|
if (isFrontend) |
||||
|
{ |
||||
|
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SetupFakeWithIdQuery(Status[] status, HashSet<Guid> ids) |
||||
|
{ |
||||
|
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
||||
|
.Returns(schema); |
||||
|
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids)) |
||||
|
.Returns(new List<IContentEntity> { content }); |
||||
|
A.CallTo(() => contentRepository.CountAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids)) |
||||
|
.Returns(123); |
||||
|
} |
||||
|
|
||||
|
private void SetupFakeWithOdataQuery(Status[] status) |
||||
|
{ |
||||
|
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
||||
|
.Returns(schema); |
||||
|
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored)) |
||||
|
.Returns(new List<IContentEntity> { content }); |
||||
|
A.CallTo(() => contentRepository.CountAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored)) |
||||
|
.Returns(123); |
||||
|
} |
||||
|
|
||||
|
private void SetupFakeWithScripting() |
||||
|
{ |
||||
|
A.CallTo(() => schema.ScriptQuery) |
||||
|
.Returns("<script-query>"); |
||||
|
|
||||
|
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "<query-script>")) |
||||
|
.Returns(transformedData); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,689 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphQLTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using NodaTime.Extensions; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.GraphQL; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.TestData; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public class GraphQLTests |
||||
|
{ |
||||
|
private static readonly Guid schemaId = Guid.NewGuid(); |
||||
|
private static readonly Guid appId = Guid.NewGuid(); |
||||
|
private static readonly string appName = "my-app"; |
||||
|
private readonly Schema schemaDef; |
||||
|
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); |
||||
|
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); |
||||
|
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>(); |
||||
|
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
||||
|
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
||||
|
private readonly IAppEntity app = A.Dummy<IAppEntity>(); |
||||
|
private readonly ClaimsPrincipal user = new ClaimsPrincipal(); |
||||
|
private readonly IGraphQLService sut; |
||||
|
|
||||
|
public GraphQLTests() |
||||
|
{ |
||||
|
schemaDef = |
||||
|
new Schema("my-schema") |
||||
|
.AddField(new JsonField(1, "my-json", Partitioning.Invariant, |
||||
|
new JsonFieldProperties())) |
||||
|
.AddField(new StringField(2, "my-string", Partitioning.Language, |
||||
|
new StringFieldProperties())) |
||||
|
.AddField(new NumberField(3, "my-number", Partitioning.Invariant, |
||||
|
new NumberFieldProperties())) |
||||
|
.AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, |
||||
|
new AssetsFieldProperties())) |
||||
|
.AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, |
||||
|
new BooleanFieldProperties())) |
||||
|
.AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, |
||||
|
new DateTimeFieldProperties())) |
||||
|
.AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, |
||||
|
new ReferencesFieldProperties { SchemaId = schemaId })) |
||||
|
.AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant, |
||||
|
new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) |
||||
|
.AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, |
||||
|
new GeolocationFieldProperties())) |
||||
|
.AddField(new TagsField(11, "my-tags", Partitioning.Invariant, |
||||
|
new TagsFieldProperties())); |
||||
|
|
||||
|
A.CallTo(() => app.Id).Returns(appId); |
||||
|
A.CallTo(() => app.Name).Returns(appName); |
||||
|
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.DE)); |
||||
|
|
||||
|
A.CallTo(() => schema.Id).Returns(schemaId); |
||||
|
A.CallTo(() => schema.Name).Returns(schemaDef.Name); |
||||
|
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
||||
|
A.CallTo(() => schema.IsPublished).Returns(true); |
||||
|
A.CallTo(() => schema.ScriptQuery).Returns("<script-query>"); |
||||
|
|
||||
|
var allSchemas = new List<ISchemaEntity> { schema }; |
||||
|
|
||||
|
A.CallTo(() => appProvider.GetSchemasAsync(appName)).Returns(allSchemas); |
||||
|
|
||||
|
sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator()); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(null)] |
||||
|
[InlineData("")] |
||||
|
[InlineData(" ")] |
||||
|
public async Task Should_return_empty_object_for_empty_query(string query) |
||||
|
{ |
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_multiple_assets_when_querying_assets() |
||||
|
{ |
||||
|
const string query = @"
|
||||
|
query { |
||||
|
queryAssets(search: ""my-query"", top: 30, skip: 5) { |
||||
|
id |
||||
|
version |
||||
|
created |
||||
|
createdBy |
||||
|
lastModified |
||||
|
lastModifiedBy |
||||
|
url |
||||
|
thumbnailUrl |
||||
|
sourceUrl |
||||
|
mimeType |
||||
|
fileName |
||||
|
fileSize |
||||
|
fileVersion |
||||
|
isImage |
||||
|
pixelWidth |
||||
|
pixelHeight |
||||
|
} |
||||
|
}";
|
||||
|
|
||||
|
var asset = CreateAsset(Guid.NewGuid()); |
||||
|
|
||||
|
var assets = new List<IAssetEntity> { asset }; |
||||
|
|
||||
|
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5)) |
||||
|
.Returns(assets); |
||||
|
|
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
queryAssets = new dynamic[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
id = asset.Id, |
||||
|
version = 1, |
||||
|
created = asset.Created.ToDateTimeUtc(), |
||||
|
createdBy = "subject:user1", |
||||
|
lastModified = asset.LastModified.ToDateTimeUtc(), |
||||
|
lastModifiedBy = "subject:user2", |
||||
|
url = $"assets/{asset.Id}", |
||||
|
thumbnailUrl = $"assets/{asset.Id}?width=100", |
||||
|
sourceUrl = $"assets/source/{asset.Id}", |
||||
|
mimeType = "image/png", |
||||
|
fileName = "MyFile.png", |
||||
|
fileSize = 1024, |
||||
|
fileVersion = 123, |
||||
|
isImage = true, |
||||
|
pixelWidth = 800, |
||||
|
pixelHeight = 600 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_single_asset_when_finding_asset() |
||||
|
{ |
||||
|
var assetId = Guid.NewGuid(); |
||||
|
var asset = CreateAsset(Guid.NewGuid()); |
||||
|
|
||||
|
var query = $@"
|
||||
|
query {{ |
||||
|
findAsset(id: ""{assetId}"") {{ |
||||
|
id |
||||
|
version |
||||
|
created |
||||
|
createdBy |
||||
|
lastModified |
||||
|
lastModifiedBy |
||||
|
url |
||||
|
thumbnailUrl |
||||
|
sourceUrl |
||||
|
mimeType |
||||
|
fileName |
||||
|
fileSize |
||||
|
fileVersion |
||||
|
isImage |
||||
|
pixelWidth |
||||
|
pixelHeight |
||||
|
}} |
||||
|
}}";
|
||||
|
|
||||
|
A.CallTo(() => assetRepository.FindAssetAsync(assetId)) |
||||
|
.Returns(asset); |
||||
|
|
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
findAsset = new |
||||
|
{ |
||||
|
id = asset.Id, |
||||
|
version = 1, |
||||
|
created = asset.Created.ToDateTimeUtc(), |
||||
|
createdBy = "subject:user1", |
||||
|
lastModified = asset.LastModified.ToDateTimeUtc(), |
||||
|
lastModifiedBy = "subject:user2", |
||||
|
url = $"assets/{asset.Id}", |
||||
|
thumbnailUrl = $"assets/{asset.Id}?width=100", |
||||
|
sourceUrl = $"assets/source/{asset.Id}", |
||||
|
mimeType = "image/png", |
||||
|
fileName = "MyFile.png", |
||||
|
fileSize = 1024, |
||||
|
fileVersion = 123, |
||||
|
isImage = true, |
||||
|
pixelWidth = 800, |
||||
|
pixelHeight = 600 |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_multiple_contents_when_querying_contents() |
||||
|
{ |
||||
|
const string query = @"
|
||||
|
query { |
||||
|
queryMySchemaContents(top: 30, skip: 5) { |
||||
|
id |
||||
|
version |
||||
|
created |
||||
|
createdBy |
||||
|
lastModified |
||||
|
lastModifiedBy |
||||
|
url |
||||
|
data { |
||||
|
myString { |
||||
|
de |
||||
|
} |
||||
|
myNumber { |
||||
|
iv |
||||
|
} |
||||
|
myBoolean { |
||||
|
iv |
||||
|
} |
||||
|
myDatetime { |
||||
|
iv |
||||
|
} |
||||
|
myJson { |
||||
|
iv |
||||
|
} |
||||
|
myGeolocation { |
||||
|
iv |
||||
|
} |
||||
|
myTags { |
||||
|
iv |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}";
|
||||
|
|
||||
|
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); |
||||
|
|
||||
|
var contents = new List<IContentEntity> { content }; |
||||
|
|
||||
|
A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, "?$top=30&$skip=5")) |
||||
|
.Returns((schema, 0L, (IReadOnlyList<IContentEntity>)contents)); |
||||
|
|
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
queryMySchemaContents = new dynamic[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
id = content.Id, |
||||
|
version = 1, |
||||
|
created = content.Created.ToDateTimeUtc(), |
||||
|
createdBy = "subject:user1", |
||||
|
lastModified = content.LastModified.ToDateTimeUtc(), |
||||
|
lastModifiedBy = "subject:user2", |
||||
|
url = $"contents/my-schema/{content.Id}", |
||||
|
data = new |
||||
|
{ |
||||
|
myString = new |
||||
|
{ |
||||
|
de = "value" |
||||
|
}, |
||||
|
myNumber = new |
||||
|
{ |
||||
|
iv = 1 |
||||
|
}, |
||||
|
myBoolean = new |
||||
|
{ |
||||
|
iv = true |
||||
|
}, |
||||
|
myDatetime = new |
||||
|
{ |
||||
|
iv = content.LastModified.ToDateTimeUtc() |
||||
|
}, |
||||
|
myJson = new |
||||
|
{ |
||||
|
iv = new |
||||
|
{ |
||||
|
value = 1 |
||||
|
} |
||||
|
}, |
||||
|
myGeolocation = new |
||||
|
{ |
||||
|
iv = new |
||||
|
{ |
||||
|
latitude = 10, |
||||
|
longitude = 20 |
||||
|
} |
||||
|
}, |
||||
|
myTags = new |
||||
|
{ |
||||
|
iv = new[] |
||||
|
{ |
||||
|
"tag1", |
||||
|
"tag2" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_single_content_when_finding_content() |
||||
|
{ |
||||
|
var contentId = Guid.NewGuid(); |
||||
|
var content = CreateContent(contentId, Guid.Empty, Guid.Empty); |
||||
|
|
||||
|
var query = $@"
|
||||
|
query {{ |
||||
|
findMySchemaContent(id: ""{contentId}"") {{ |
||||
|
id |
||||
|
version |
||||
|
created |
||||
|
createdBy |
||||
|
lastModified |
||||
|
lastModifiedBy |
||||
|
url |
||||
|
data {{ |
||||
|
myString {{ |
||||
|
de |
||||
|
}} |
||||
|
myNumber {{ |
||||
|
iv |
||||
|
}} |
||||
|
myBoolean {{ |
||||
|
iv |
||||
|
}} |
||||
|
myDatetime {{ |
||||
|
iv |
||||
|
}} |
||||
|
myJson {{ |
||||
|
iv |
||||
|
}} |
||||
|
myGeolocation {{ |
||||
|
iv |
||||
|
}} |
||||
|
myTags {{ |
||||
|
iv |
||||
|
}} |
||||
|
}} |
||||
|
}} |
||||
|
}}";
|
||||
|
|
||||
|
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
||||
|
.Returns((schema, content)); |
||||
|
|
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
findMySchemaContent = new |
||||
|
{ |
||||
|
id = content.Id, |
||||
|
version = 1, |
||||
|
created = content.Created.ToDateTimeUtc(), |
||||
|
createdBy = "subject:user1", |
||||
|
lastModified = content.LastModified.ToDateTimeUtc(), |
||||
|
lastModifiedBy = "subject:user2", |
||||
|
url = $"contents/my-schema/{content.Id}", |
||||
|
data = new |
||||
|
{ |
||||
|
myString = new |
||||
|
{ |
||||
|
de = "value" |
||||
|
}, |
||||
|
myNumber = new |
||||
|
{ |
||||
|
iv = 1 |
||||
|
}, |
||||
|
myBoolean = new |
||||
|
{ |
||||
|
iv = true |
||||
|
}, |
||||
|
myDatetime = new |
||||
|
{ |
||||
|
iv = content.LastModified.ToDateTimeUtc() |
||||
|
}, |
||||
|
myJson = new |
||||
|
{ |
||||
|
iv = new |
||||
|
{ |
||||
|
value = 1 |
||||
|
} |
||||
|
}, |
||||
|
myGeolocation = new |
||||
|
{ |
||||
|
iv = new |
||||
|
{ |
||||
|
latitude = 10, |
||||
|
longitude = 20 |
||||
|
} |
||||
|
}, |
||||
|
myTags = new |
||||
|
{ |
||||
|
iv = new[] |
||||
|
{ |
||||
|
"tag1", |
||||
|
"tag2" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() |
||||
|
{ |
||||
|
var contentRefId = Guid.NewGuid(); |
||||
|
var contentRef = CreateContent(contentRefId, Guid.Empty, Guid.Empty); |
||||
|
|
||||
|
var contentId = Guid.NewGuid(); |
||||
|
var content = CreateContent(contentId, contentRefId, Guid.Empty); |
||||
|
|
||||
|
var query = $@"
|
||||
|
query {{ |
||||
|
findMySchemaContent(id: ""{contentId}"") {{ |
||||
|
id |
||||
|
data {{ |
||||
|
myReferences {{ |
||||
|
iv {{ |
||||
|
id |
||||
|
}} |
||||
|
}} |
||||
|
}} |
||||
|
}} |
||||
|
}}";
|
||||
|
|
||||
|
var refContents = new List<IContentEntity> { contentRef }; |
||||
|
|
||||
|
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
||||
|
.Returns((schema, content)); |
||||
|
|
||||
|
A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A<HashSet<Guid>>.That.Matches(x => x.Contains(contentRefId)))) |
||||
|
.Returns((schema, 0L, (IReadOnlyList<IContentEntity>)refContents)); |
||||
|
|
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
findMySchemaContent = new |
||||
|
{ |
||||
|
id = content.Id, |
||||
|
data = new |
||||
|
{ |
||||
|
myReferences = new |
||||
|
{ |
||||
|
iv = new[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
id = contentRefId |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() |
||||
|
{ |
||||
|
var assetRefId = Guid.NewGuid(); |
||||
|
var assetRef = CreateAsset(assetRefId); |
||||
|
|
||||
|
var contentId = Guid.NewGuid(); |
||||
|
var content = CreateContent(contentId, Guid.Empty, assetRefId); |
||||
|
|
||||
|
var query = $@"
|
||||
|
query {{ |
||||
|
findMySchemaContent(id: ""{contentId}"") {{ |
||||
|
id |
||||
|
data {{ |
||||
|
myAssets {{ |
||||
|
iv {{ |
||||
|
id |
||||
|
}} |
||||
|
}} |
||||
|
}} |
||||
|
}} |
||||
|
}}";
|
||||
|
|
||||
|
var refAssets = new List<IAssetEntity> { assetRef }; |
||||
|
|
||||
|
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
||||
|
.Returns((schema, content)); |
||||
|
|
||||
|
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A<HashSet<Guid>>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) |
||||
|
.Returns(refAssets); |
||||
|
|
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = new |
||||
|
{ |
||||
|
findMySchemaContent = new |
||||
|
{ |
||||
|
id = content.Id, |
||||
|
data = new |
||||
|
{ |
||||
|
myAssets = new |
||||
|
{ |
||||
|
iv = new[] |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
id = assetRefId |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_return_data_when_field_not_part_of_content() |
||||
|
{ |
||||
|
var contentId = Guid.NewGuid(); |
||||
|
var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); |
||||
|
|
||||
|
var query = $@"
|
||||
|
query {{ |
||||
|
findMySchemaContent(id: ""{contentId}"") {{ |
||||
|
id |
||||
|
version |
||||
|
created |
||||
|
createdBy |
||||
|
lastModified |
||||
|
lastModifiedBy |
||||
|
url |
||||
|
data {{ |
||||
|
myInvalid {{ |
||||
|
iv |
||||
|
}} |
||||
|
}} |
||||
|
}} |
||||
|
}}";
|
||||
|
|
||||
|
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
||||
|
.Returns((schema, content)); |
||||
|
|
||||
|
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
||||
|
|
||||
|
var expected = new |
||||
|
{ |
||||
|
data = (object)null |
||||
|
}; |
||||
|
|
||||
|
AssertJson(expected, new { data = result.Data }); |
||||
|
} |
||||
|
|
||||
|
private static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null) |
||||
|
{ |
||||
|
var now = DateTime.UtcNow.ToInstant(); |
||||
|
|
||||
|
data = data ?? |
||||
|
new NamedContentData() |
||||
|
.AddField("my-json", |
||||
|
new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 }))) |
||||
|
.AddField("my-string", |
||||
|
new ContentFieldData().AddValue("de", "value")) |
||||
|
.AddField("my-assets", |
||||
|
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId }))) |
||||
|
.AddField("my-number", |
||||
|
new ContentFieldData().AddValue("iv", 1)) |
||||
|
.AddField("my-boolean", |
||||
|
new ContentFieldData().AddValue("iv", true)) |
||||
|
.AddField("my-datetime", |
||||
|
new ContentFieldData().AddValue("iv", now.ToDateTimeUtc())) |
||||
|
.AddField("my-tags", |
||||
|
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" }))) |
||||
|
.AddField("my-references", |
||||
|
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId }))) |
||||
|
.AddField("my-geolocation", |
||||
|
new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 }))); |
||||
|
|
||||
|
var content = new FakeContentEntity |
||||
|
{ |
||||
|
Id = id, |
||||
|
Version = 1, |
||||
|
Created = now, |
||||
|
CreatedBy = new RefToken("subject", "user1"), |
||||
|
LastModified = now, |
||||
|
LastModifiedBy = new RefToken("subject", "user2"), |
||||
|
Data = data |
||||
|
}; |
||||
|
|
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
private static IAssetEntity CreateAsset(Guid id) |
||||
|
{ |
||||
|
var now = DateTime.UtcNow.ToInstant(); |
||||
|
|
||||
|
var asset = new FakeAssetEntity |
||||
|
{ |
||||
|
Id = id, |
||||
|
Version = 1, |
||||
|
Created = now, |
||||
|
CreatedBy = new RefToken("subject", "user1"), |
||||
|
LastModified = now, |
||||
|
LastModifiedBy = new RefToken("subject", "user2"), |
||||
|
FileName = "MyFile.png", |
||||
|
FileSize = 1024, |
||||
|
FileVersion = 123, |
||||
|
MimeType = "image/png", |
||||
|
IsImage = true, |
||||
|
PixelWidth = 800, |
||||
|
PixelHeight = 600 |
||||
|
}; |
||||
|
|
||||
|
return asset; |
||||
|
} |
||||
|
|
||||
|
private static void AssertJson(object expected, object result) |
||||
|
{ |
||||
|
var resultJson = JsonConvert.SerializeObject(result, Formatting.Indented); |
||||
|
var expectJson = JsonConvert.SerializeObject(expected, Formatting.Indented); |
||||
|
|
||||
|
Assert.Equal(expectJson, resultJson); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,394 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ODataQueryTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
/* |
||||
|
using System; |
||||
|
using System.Collections.Immutable; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Microsoft.OData.Edm; |
||||
|
using MongoDB.Bson.Serialization; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Edm; |
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public class ODataQueryTests |
||||
|
{ |
||||
|
private readonly Schema schemaDef; |
||||
|
private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; |
||||
|
private readonly IBsonSerializer<MongoContentEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); |
||||
|
private readonly IEdmModel edmModel; |
||||
|
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); |
||||
|
|
||||
|
static ODataQueryTests() |
||||
|
{ |
||||
|
InstantSerializer.Register(); |
||||
|
} |
||||
|
|
||||
|
public ODataQueryTests() |
||||
|
{ |
||||
|
schemaDef = |
||||
|
new Schema("user") |
||||
|
.AddField(new StringField(1, "firstName", Partitioning.Language, |
||||
|
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })) |
||||
|
.AddField(new StringField(2, "lastName", Partitioning.Language, |
||||
|
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })) |
||||
|
.AddField(new BooleanField(3, "isAdmin", Partitioning.Invariant, |
||||
|
new BooleanFieldProperties())) |
||||
|
.AddField(new NumberField(4, "age", Partitioning.Invariant, |
||||
|
new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) |
||||
|
.AddField(new DateTimeField(5, "birthday", Partitioning.Invariant, |
||||
|
new DateTimeFieldProperties())) |
||||
|
.AddField(new AssetsField(6, "pictures", Partitioning.Invariant, |
||||
|
new AssetsFieldProperties())) |
||||
|
.AddField(new ReferencesField(7, "friends", Partitioning.Invariant, |
||||
|
new ReferencesFieldProperties())) |
||||
|
.AddField(new StringField(8, "dashed-field", Partitioning.Invariant, |
||||
|
new StringFieldProperties())) |
||||
|
.Update(new SchemaProperties { Hints = "The User" }); |
||||
|
|
||||
|
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); |
||||
|
|
||||
|
var schema = A.Dummy<ISchemaEntity>(); |
||||
|
A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); |
||||
|
A.CallTo(() => schema.Version).Returns(3); |
||||
|
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
||||
|
|
||||
|
var app = A.Dummy<IAppEntity>(); |
||||
|
A.CallTo(() => app.Id).Returns(Guid.NewGuid()); |
||||
|
A.CallTo(() => app.Version).Returns(3); |
||||
|
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); |
||||
|
|
||||
|
edmModel = builder.BuildEdmModel(schema, app); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_query() |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); |
||||
|
|
||||
|
Assert.NotNull(parser); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_query_with_underscore_field() |
||||
|
{ |
||||
|
var i = F("$filter=data/dashed_field/iv eq 'Value'"); |
||||
|
var o = C("{ 'do.8.iv' : 'Value' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_not_operator() |
||||
|
{ |
||||
|
var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')"); |
||||
|
var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_starts_with_query() |
||||
|
{ |
||||
|
var i = F("$filter=startswith(data/firstName/de, 'Sebastian')"); |
||||
|
var o = C("{ 'do.1.de' : /^Sebastian/i }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_ends_with_query() |
||||
|
{ |
||||
|
var i = F("$filter=endswith(data/firstName/de, 'Sebastian')"); |
||||
|
var o = C("{ 'do.1.de' : /Sebastian$/i }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_contains_query() |
||||
|
{ |
||||
|
var i = F("$filter=contains(data/firstName/de, 'Sebastian')"); |
||||
|
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_contains_query_with_equals() |
||||
|
{ |
||||
|
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true"); |
||||
|
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_negated_contains_query_with_equals() |
||||
|
{ |
||||
|
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false"); |
||||
|
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_negated_contains_query_and_other() |
||||
|
{ |
||||
|
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true"); |
||||
|
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_string_equals_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/firstName/de eq 'Sebastian'"); |
||||
|
var o = C("{ 'do.1.de' : 'Sebastian' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_datetime_equals_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z"); |
||||
|
var o = C("{ 'do.5.iv' : ISODate(\"1988-01-19T12:00:00Z\") }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_boolean_equals_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/isAdmin/iv eq true"); |
||||
|
var o = C("{ 'do.3.iv' : true }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_string_not_equals_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/firstName/de ne 'Sebastian'"); |
||||
|
var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_number_less_than_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/age/iv lt 1"); |
||||
|
var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_number_less_equals_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/age/iv le 1"); |
||||
|
var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_number_greater_than_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/age/iv gt 1"); |
||||
|
var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_number_greater_equals_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/age/iv ge 1"); |
||||
|
var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_equals_query_for_assets() |
||||
|
{ |
||||
|
var i = F("$filter=data/pictures/iv eq 'guid'"); |
||||
|
var o = C("{ 'do.6.iv' : 'guid' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_equals_query_for_references() |
||||
|
{ |
||||
|
var i = F("$filter=data/friends/iv eq 'guid'"); |
||||
|
var o = C("{ 'do.7.iv' : 'guid' }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_and_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2"); |
||||
|
var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_or_query() |
||||
|
{ |
||||
|
var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2"); |
||||
|
var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_full_text_query() |
||||
|
{ |
||||
|
var i = F("$search=Hello my World"); |
||||
|
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_full_text_query_with_and() |
||||
|
{ |
||||
|
var i = F("$search=A and B"); |
||||
|
var o = C("{ '$text' : { '$search' : 'A and B' } }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_orderby_with_single_statements() |
||||
|
{ |
||||
|
var i = S("$orderby=data/age/iv desc"); |
||||
|
var o = C("{ 'do.4.iv' : -1 }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_convert_orderby_with_multiple_statements() |
||||
|
{ |
||||
|
var i = S("$orderby=data/age/iv, data/firstName/en desc"); |
||||
|
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); |
||||
|
|
||||
|
Assert.Equal(o, i); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_set_top() |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery("$top=3"); |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
cursor.Take(parser); |
||||
|
|
||||
|
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_set_max_top_if_larger() |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery("$top=300"); |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
cursor.Take(parser); |
||||
|
|
||||
|
A.CallTo(() => cursor.Limit(200)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_set_default_top() |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery(string.Empty); |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
cursor.Take(parser); |
||||
|
|
||||
|
A.CallTo(() => cursor.Limit(20)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_set_skip() |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery("$skip=3"); |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
cursor.Skip(parser); |
||||
|
|
||||
|
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_set_skip() |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery(string.Empty); |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
cursor.Take(parser); |
||||
|
|
||||
|
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private static string C(string value) |
||||
|
{ |
||||
|
return value.Replace('\'', '"'); |
||||
|
} |
||||
|
|
||||
|
private string S(string value) |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery(value); |
||||
|
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
||||
|
|
||||
|
var i = string.Empty; |
||||
|
|
||||
|
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored)) |
||||
|
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) => |
||||
|
{ |
||||
|
i = sortDefinition.Render(serializer, registry).ToString(); |
||||
|
}); |
||||
|
|
||||
|
cursor.Sort(parser, schemaDef); |
||||
|
|
||||
|
return i; |
||||
|
} |
||||
|
|
||||
|
private string F(string value) |
||||
|
{ |
||||
|
var parser = edmModel.ParseQuery(value); |
||||
|
|
||||
|
var query = FilterBuilder.Build(parser, schemaDef).Render(serializer, registry).ToString(); |
||||
|
|
||||
|
return query; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
*/ |
||||
@ -0,0 +1,50 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MockupAssetEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.TestData |
||||
|
{ |
||||
|
public sealed class FakeAssetEntity : IAssetEntity |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
public Guid AssetId { get; set; } |
||||
|
|
||||
|
public Instant Created { get; set; } |
||||
|
|
||||
|
public Instant LastModified { get; set; } |
||||
|
|
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
|
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
public string MimeType { get; set; } |
||||
|
|
||||
|
public string FileName { get; set; } |
||||
|
|
||||
|
public long FileSize { get; set; } |
||||
|
|
||||
|
public long FileVersion { get; set; } |
||||
|
|
||||
|
public bool IsImage { get; set; } |
||||
|
|
||||
|
public bool IsDeleted { get; set; } |
||||
|
|
||||
|
public int? PixelWidth { get; set; } |
||||
|
|
||||
|
public int? PixelHeight { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
// ==========================================================================
|
||||
|
// FakeContentEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.TestData |
||||
|
{ |
||||
|
public sealed class FakeContentEntity : IContentEntity |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
public Instant Created { get; set; } |
||||
|
|
||||
|
public Instant LastModified { get; set; } |
||||
|
|
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
|
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
public NamedContentData Data { get; set; } |
||||
|
|
||||
|
public Status Status { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
// ==========================================================================
|
||||
|
// FakeUrlGenerator.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.GraphQL; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.TestData |
||||
|
{ |
||||
|
public sealed class FakeUrlGenerator : IGraphQLUrlGenerator |
||||
|
{ |
||||
|
public bool CanGenerateAssetSourceUrl { get; } = true; |
||||
|
|
||||
|
public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) |
||||
|
{ |
||||
|
return $"assets/{asset.Id}"; |
||||
|
} |
||||
|
|
||||
|
public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) |
||||
|
{ |
||||
|
return $"assets/{asset.Id}?width=100"; |
||||
|
} |
||||
|
|
||||
|
public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) |
||||
|
{ |
||||
|
return $"assets/source/{asset.Id}"; |
||||
|
} |
||||
|
|
||||
|
public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) |
||||
|
{ |
||||
|
return $"contents/{schema.Name}/{content.Id}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
// ==========================================================================
|
||||
|
// RuleDequeuerGrainTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Repositories; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public class RuleDequeuerTests |
||||
|
{ |
||||
|
private readonly IClock clock = A.Fake<IClock>(); |
||||
|
private readonly ISemanticLog log = A.Fake<ISemanticLog>(); |
||||
|
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
||||
|
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); |
||||
|
private readonly RuleService ruleService = A.Fake<RuleService>(); |
||||
|
private readonly RuleDequeuer sut; |
||||
|
private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); |
||||
|
|
||||
|
public RuleDequeuerTests() |
||||
|
{ |
||||
|
A.CallTo(() => clock.GetCurrentInstant()).Returns(now); |
||||
|
|
||||
|
sut = new RuleDequeuer( |
||||
|
ruleService, |
||||
|
ruleEventRepository, |
||||
|
log, |
||||
|
clock); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] |
||||
|
[InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] |
||||
|
[InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] |
||||
|
[InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] |
||||
|
[InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] |
||||
|
[InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] |
||||
|
public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) |
||||
|
{ |
||||
|
var actionData = new RuleJobData(); |
||||
|
var actionName = "MyAction"; |
||||
|
|
||||
|
var @event = CreateEvent(calls, actionName, actionData); |
||||
|
|
||||
|
var requestElapsed = TimeSpan.FromMinutes(1); |
||||
|
var requestDump = "Dump"; |
||||
|
|
||||
|
A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData)) |
||||
|
.Returns((requestDump, result, requestElapsed)); |
||||
|
|
||||
|
Instant? nextCall = null; |
||||
|
|
||||
|
if (minutes > 0) |
||||
|
{ |
||||
|
nextCall = now.Plus(Duration.FromMinutes(minutes)); |
||||
|
} |
||||
|
|
||||
|
await sut.HandleAsync(@event); |
||||
|
|
||||
|
sut.Dispose(); |
||||
|
|
||||
|
A.CallTo(() => ruleEventRepository.MarkSentAsync(@event.Id, requestDump, result, jobResult, requestElapsed, nextCall)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData) |
||||
|
{ |
||||
|
var @event = A.Fake<IRuleEventEntity>(); |
||||
|
|
||||
|
var job = new RuleJob |
||||
|
{ |
||||
|
RuleId = Guid.NewGuid(), |
||||
|
ActionData = actionData, |
||||
|
ActionName = actionName, |
||||
|
Created = now |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); |
||||
|
A.CallTo(() => @event.Job).Returns(job); |
||||
|
A.CallTo(() => @event.Created).Returns(now); |
||||
|
A.CallTo(() => @event.NumCalls).Returns(numCalls); |
||||
|
|
||||
|
return @event; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,102 @@ |
|||||
|
// ==========================================================================
|
||||
|
// RuleEnqueuerTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Actions; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Repositories; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public class RuleEnqueuerTests |
||||
|
{ |
||||
|
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
||||
|
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); |
||||
|
private readonly RuleService ruleService = A.Fake<RuleService>(); |
||||
|
private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); |
||||
|
private readonly NamedId<Guid> appId = new NamedId<Guid>(Guid.NewGuid(), "my-app"); |
||||
|
private readonly RuleEnqueuer sut; |
||||
|
|
||||
|
public RuleEnqueuerTests() |
||||
|
{ |
||||
|
sut = new RuleEnqueuer( |
||||
|
ruleEventRepository, |
||||
|
appProvider, |
||||
|
ruleService); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_contents_filter_for_events_filter() |
||||
|
{ |
||||
|
Assert.Equal(".*", sut.EventsFilter); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_type_name_for_name() |
||||
|
{ |
||||
|
Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public Task Should_do_nothing_on_clear() |
||||
|
{ |
||||
|
return sut.ClearAsync(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_update_repositories_on_with_jobs_from_sender() |
||||
|
{ |
||||
|
var @event = Envelope.Create(new ContentCreated { AppId = appId }); |
||||
|
|
||||
|
var rule1 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); |
||||
|
var rule2 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); |
||||
|
var rule3 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); |
||||
|
|
||||
|
var job1 = new RuleJob { Created = now }; |
||||
|
var job2 = new RuleJob { Created = now }; |
||||
|
|
||||
|
var ruleEntity1 = A.Fake<IRuleEntity>(); |
||||
|
var ruleEntity2 = A.Fake<IRuleEntity>(); |
||||
|
var ruleEntity3 = A.Fake<IRuleEntity>(); |
||||
|
|
||||
|
A.CallTo(() => ruleEntity1.RuleDef).Returns(rule1); |
||||
|
A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2); |
||||
|
A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3); |
||||
|
|
||||
|
A.CallTo(() => appProvider.GetRulesAsync(appId.Name)) |
||||
|
.Returns(new List<IRuleEntity> { ruleEntity1, ruleEntity2, ruleEntity3 }); |
||||
|
|
||||
|
A.CallTo(() => ruleService.CreateJob(rule1, @event)) |
||||
|
.Returns(job1); |
||||
|
|
||||
|
A.CallTo(() => ruleService.CreateJob(rule2, @event)) |
||||
|
.Returns(job2); |
||||
|
|
||||
|
A.CallTo(() => ruleService.CreateJob(rule3, @event)) |
||||
|
.Returns(null); |
||||
|
|
||||
|
await sut.On(@event); |
||||
|
|
||||
|
A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => ruleEventRepository.EnqueueAsync(job2, now)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<OutputType>Exe</OutputType> |
||||
|
<TargetFramework>netcoreapp2.0</TargetFramework> |
||||
|
<RootNamespace>Squidex.Domain.Apps.Entities</RootNamespace> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<Compile Remove="MongoDb\**" /> |
||||
|
<EmbeddedResource Remove="MongoDb\**" /> |
||||
|
<None Remove="MongoDb\**" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="FakeItEasy" Version="4.2.0" /> |
||||
|
<PackageReference Include="FluentAssertions" Version="4.19.4" /> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" /> |
||||
|
<PackageReference Include="MongoDB.Driver" Version="2.4.4" /> |
||||
|
<PackageReference Include="RefactoringEssentials" Version="5.4.0" /> |
||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> |
||||
|
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> |
||||
|
<PackageReference Include="xunit" Version="2.3.1" /> |
||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" /> |
||||
|
</ItemGroup> |
||||
|
<PropertyGroup> |
||||
|
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> |
||||
|
</PropertyGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,45 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AssertHelper.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.TestHelpers |
||||
|
{ |
||||
|
public static class AssertHelper |
||||
|
{ |
||||
|
public static void ShouldHaveSameEvents(this IEnumerable<Envelope<IEvent>> events, params IEvent[] others) |
||||
|
{ |
||||
|
var source = events.Select(x => x.Payload).ToArray(); |
||||
|
|
||||
|
source.Should().HaveSameCount(others); |
||||
|
|
||||
|
for (var i = 0; i < source.Length; i++) |
||||
|
{ |
||||
|
var lhs = source[i]; |
||||
|
var rhs = others[i]; |
||||
|
|
||||
|
lhs.ShouldBeSameEvent(rhs); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) |
||||
|
{ |
||||
|
lhs.Should().BeOfType(rhs.GetType()); |
||||
|
|
||||
|
((object)lhs).ShouldBeEquivalentTo(rhs, o => o.IncludingAllDeclaredProperties()); |
||||
|
} |
||||
|
|
||||
|
public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs) |
||||
|
{ |
||||
|
lhs.Should().BeOfType(rhs.GetType()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,162 @@ |
|||||
|
// ==========================================================================
|
||||
|
// HandlerTestBase.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
|
||||
|
#pragma warning disable IDE0019 // Use pattern matching
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.TestHelpers |
||||
|
{ |
||||
|
public abstract class HandlerTestBase<T> where T : IDomainObject |
||||
|
{ |
||||
|
private sealed class MockupHandler : IAggregateHandler |
||||
|
{ |
||||
|
private T domainObject; |
||||
|
|
||||
|
public bool IsCreated { get; private set; } |
||||
|
public bool IsUpdated { get; private set; } |
||||
|
|
||||
|
public void Init(T newDomainObject) |
||||
|
{ |
||||
|
domainObject = newDomainObject; |
||||
|
|
||||
|
IsCreated = false; |
||||
|
IsUpdated = false; |
||||
|
} |
||||
|
|
||||
|
public async Task<V> CreateAsync<V>(CommandContext context, Func<V, Task> creator) where V : class, IDomainObject |
||||
|
{ |
||||
|
IsCreated = true; |
||||
|
|
||||
|
var @do = domainObject as V; |
||||
|
|
||||
|
await creator(domainObject as V); |
||||
|
|
||||
|
return @do; |
||||
|
} |
||||
|
|
||||
|
public async Task<V> UpdateAsync<V>(CommandContext context, Func<V, Task> updater) where V : class, IDomainObject |
||||
|
{ |
||||
|
IsUpdated = true; |
||||
|
|
||||
|
var @do = domainObject as V; |
||||
|
|
||||
|
await updater(domainObject as V); |
||||
|
|
||||
|
return @do; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly MockupHandler handler = new MockupHandler(); |
||||
|
|
||||
|
protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString()); |
||||
|
|
||||
|
protected Guid AppId { get; } = Guid.NewGuid(); |
||||
|
|
||||
|
protected Guid SchemaId { get; } = Guid.NewGuid(); |
||||
|
|
||||
|
protected string AppName { get; } = "my-app"; |
||||
|
|
||||
|
protected string SchemaName { get; } = "my-schema"; |
||||
|
|
||||
|
protected NamedId<Guid> AppNamedId |
||||
|
{ |
||||
|
get { return new NamedId<Guid>(AppId, AppName); } |
||||
|
} |
||||
|
|
||||
|
protected NamedId<Guid> SchemaNamedId |
||||
|
{ |
||||
|
get { return new NamedId<Guid>(SchemaId, SchemaName); } |
||||
|
} |
||||
|
|
||||
|
protected IAggregateHandler Handler |
||||
|
{ |
||||
|
get { return handler; } |
||||
|
} |
||||
|
|
||||
|
protected CommandContext CreateContextForCommand<TCommand>(TCommand command) where TCommand : SquidexCommand |
||||
|
{ |
||||
|
return new CommandContext(CreateCommand(command)); |
||||
|
} |
||||
|
|
||||
|
protected async Task TestCreate(T domainObject, Func<T, Task> action, bool shouldCreate = true) |
||||
|
{ |
||||
|
handler.Init(domainObject); |
||||
|
|
||||
|
await action(domainObject); |
||||
|
|
||||
|
if (!handler.IsCreated && shouldCreate) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Create not called."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async Task TestUpdate(T domainObject, Func<T, Task> action, bool shouldUpdate = true) |
||||
|
{ |
||||
|
handler.Init(domainObject); |
||||
|
|
||||
|
await action(domainObject); |
||||
|
|
||||
|
if (!handler.IsUpdated && shouldUpdate) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Update not called."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected TCommand CreateCommand<TCommand>(TCommand command) where TCommand : SquidexCommand |
||||
|
{ |
||||
|
if (command.Actor == null) |
||||
|
{ |
||||
|
command.Actor = User; |
||||
|
} |
||||
|
|
||||
|
var appCommand = command as AppCommand; |
||||
|
|
||||
|
if (appCommand != null && appCommand.AppId == null) |
||||
|
{ |
||||
|
appCommand.AppId = AppNamedId; |
||||
|
} |
||||
|
|
||||
|
var schemaCommand = command as SchemaCommand; |
||||
|
|
||||
|
if (schemaCommand != null && schemaCommand.SchemaId == null) |
||||
|
{ |
||||
|
schemaCommand.SchemaId = SchemaNamedId; |
||||
|
} |
||||
|
|
||||
|
return command; |
||||
|
} |
||||
|
|
||||
|
protected TEvent CreateEvent<TEvent>(TEvent @event) where TEvent : SquidexEvent |
||||
|
{ |
||||
|
@event.Actor = User; |
||||
|
|
||||
|
var appEvent = @event as AppEvent; |
||||
|
|
||||
|
if (appEvent != null) |
||||
|
{ |
||||
|
appEvent.AppId = AppNamedId; |
||||
|
} |
||||
|
|
||||
|
var schemaEvent = @event as SchemaEvent; |
||||
|
|
||||
|
if (schemaEvent != null) |
||||
|
{ |
||||
|
schemaEvent.SchemaId = SchemaNamedId; |
||||
|
} |
||||
|
|
||||
|
return @event; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma warning restore IDE0019 // Use pattern matching
|
||||
Loading…
Reference in new issue