diff --git a/.testrunsettings b/.testrunsettings new file mode 100644 index 000000000..0082141a9 --- /dev/null +++ b/.testrunsettings @@ -0,0 +1,6 @@ + + + + 4 + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7941618aa..9037de7bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## v2.1.0 - 2019-06-22 + +## Features + +* **Assets**: Parameter to prevent download in Browser. +* **Assets**: FTP asset store. +* **GraphQL**: Logging for field resolvers +* **GraphQL**: Performance optimizations for asset fields and references with DataLoader. +* **MongoDB**: Performance optimizations. +* **MongoDB**: Support for AWS DocumentDB. +* **Schemas**: Separator field. +* **Schemas**: Setting to prevent duplicate references. +* **UI**: Improved styling of DateTime editor. +* **UI**: Custom Editors: Provide all values. +* **UI**: Custom Editors: Provide context with user information and auth token. +* **UI**: Filter by status. +* **UI**: Dropdown field for references. +* **Users**: Email notifications when contributors is added. + +## Bugfixes + +* **Contents**: Fix for scheduled publishing. +* **GraphQL**: Fix query parameters for assets. +* **GraphQL**: Fix for duplicate field names in GraphQL. +* **GraphQL**: Fix for invalid field names. +* **Plans**: Fix when plans reset and extra events. +* **UI**: Unify slugify in Frontend and Backend. + ## v2.0.5 - 2019-04-21 ## Features diff --git a/Dockerfile b/Dockerfile index 86485f677..e57b35d67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,20 @@ FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder WORKDIR /src +# Copy Node project files. COPY src/Squidex/package*.json /tmp/ # Install Node packages RUN cd /tmp && npm install --loglevel=error +# Copy nuget project files. +COPY /**/**/*.csproj /tmp/ +# Copy nuget.config for package sources. +COPY NuGet.Config /tmp/ + +# Install nuget packages +RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd' + COPY . . # Build Frontend @@ -19,8 +28,7 @@ RUN cp -a /tmp/node_modules src/Squidex/ \ && npm run build # Test Backend -RUN dotnet restore \ - && dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ +RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \ && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \ diff --git a/Dockerfile.build b/Dockerfile.build index 30a5d3091..96debc8cd 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -2,11 +2,20 @@ FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder WORKDIR /src +# Copy Node project files. COPY src/Squidex/package*.json /tmp/ # Install Node packages RUN cd /tmp && npm install --loglevel=error +# Copy Dotnet project files. +COPY /**/**/*.csproj /tmp/ +# Copy nuget.config for package sources. +COPY NuGet.Config /tmp/ + +# Install Dotnet packages +RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd' + COPY . . # Build Frontend @@ -16,8 +25,7 @@ RUN cp -a /tmp/node_modules src/Squidex/ \ && npm run build # Test Backend -RUN dotnet restore \ - && dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ +RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \ && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \ diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 44d93d8d4..6444ba638 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -119,12 +119,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified); - var assetItems = find.ToListAsync(); - var assetCount = find.CountDocumentsAsync(); + var assetItems = await find.ToListAsync(); - await Task.WhenAll(assetItems, assetCount); - - return ResultList.Create(assetCount.Result, assetItems.Result.OfType()); + return ResultList.Create(assetItems.Count, assetItems.OfType()); } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 1bb4c6128..9424333c0 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -137,17 +137,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); - var contentItems = find.WithoutDraft(includeDraft).ToListAsync(); - var contentCount = find.CountDocumentsAsync(); - - await Task.WhenAll(contentItems, contentCount); + var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); - foreach (var entity in contentItems.Result) + foreach (var entity in contentItems) { entity.ParseData(schema.SchemaDef, serializer); } - return ResultList.Create(contentCount.Result, contentItems.Result); + return ResultList.Create(contentItems.Count, contentItems); } public async Task FindContentAsync(ISchemaEntity schema, Guid id, Status[] status, bool includeDraft) diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index b13c99f29..e45e02645 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Security.Cryptography; using System.Threading.Tasks; using Orleans; +using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; @@ -24,25 +25,28 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetQueryService assetQuery; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IEnumerable> tagGenerators; + private readonly ITagService tagService; public AssetCommandMiddleware( IGrainFactory grainFactory, IAssetQueryService assetQuery, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator, - IEnumerable> tagGenerators) + IEnumerable> tagGenerators, + ITagService tagService) : base(grainFactory) { Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(tagGenerators, nameof(tagGenerators)); + Guard.NotNull(tagService, nameof(tagService)); this.assetStore = assetStore; this.assetQuery = assetQuery; this.assetThumbnailGenerator = assetThumbnailGenerator; - this.tagGenerators = tagGenerators; + this.tagService = tagService; } public override async Task HandleAsync(CommandContext context, Func next) @@ -56,9 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Assets createAsset.Tags = new HashSet(); } - createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead()); - - createAsset.FileHash = await UploadAsync(context, createAsset.File); + await EnrichWithImageInfosAsync(createAsset); + await EnrichWithHashAndUploadAsync(createAsset, context); try { @@ -70,7 +73,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { if (IsDuplicate(createAsset, existing)) { - result = new AssetCreatedResult(existing, true); + var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags); + + result = new AssetCreatedResult(existing, true, new HashSet(denormalizedTags.Values)); } break; @@ -85,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset); - result = new AssetCreatedResult(asset, false); + result = new AssetCreatedResult(asset, false, createAsset.Tags); await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null); } @@ -102,16 +107,16 @@ namespace Squidex.Domain.Apps.Entities.Assets case UpdateAsset updateAsset: { - updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead()); + await EnrichWithImageInfosAsync(updateAsset); + await EnrichWithHashAndUploadAsync(updateAsset, context); - updateAsset.FileHash = await UploadAsync(context, updateAsset.File); try { - var result = (IAssetEntity)await ExecuteCommandAsync(updateAsset); + var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset); context.Complete(result); - await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null); + await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null); } finally { @@ -121,29 +126,54 @@ namespace Squidex.Domain.Apps.Entities.Assets break; } + case AssetCommand command: + { + var result = await ExecuteAndAdjustTagsAsync(command); + + context.Complete(result); + + break; + } + default: await base.HandleAsync(context, next); + break; } } + private async Task ExecuteAndAdjustTagsAsync(AssetCommand command) + { + var result = await ExecuteCommandAsync(command); + + if (result is IAssetEntity asset) + { + var denormalizedTags = await tagService.DenormalizeTagsAsync(asset.AppId.Id, TagGroups.Assets, asset.Tags); + + return new AssetResult(asset, new HashSet(denormalizedTags.Values)); + } + + return result; + } + private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset) { return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize; } - private async Task UploadAsync(CommandContext context, AssetFile file) + private async Task EnrichWithImageInfosAsync(UploadAssetCommand command) { - string hash; + command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); + } - using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256)) + private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, CommandContext context) + { + using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256)) { await assetStore.UploadAsync(context.ContextId.ToString(), hashStream); - hash = $"{hashStream.GetHashStringAndReset()}{file.FileName}{file.FileSize}".Sha256Base64(); + command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64(); } - - return hash; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs index b1502786a..9ccc00763 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; + namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetCreatedResult + public sealed class AssetCreatedResult : AssetResult { - public IAssetEntity Asset { get; } - public bool IsDuplicate { get; } - public AssetCreatedResult(IAssetEntity asset, bool isDuplicate) + public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet tags) + : base(asset, tags) { - Asset = asset; - IsDuplicate = isDuplicate; } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs new file mode 100644 index 000000000..b43713da5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetResult + { + public IAssetEntity Asset { get; } + + public HashSet Tags { get; } + + public AssetResult(IAssetEntity asset, HashSet tags) + { + Asset = asset; + + Tags = tags; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 9c49e67bd..8e869ba40 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -8,22 +8,15 @@ using System; using System.Collections.Generic; using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public sealed class CreateAsset : AssetCommand, IAppCommand + public sealed class CreateAsset : UploadAssetCommand, IAppCommand { public NamedId AppId { get; set; } - public AssetFile File { get; set; } - - public ImageInfo ImageInfo { get; set; } - public HashSet Tags { get; set; } - public string FileHash { get; set; } - public CreateAsset() { AssetId = Guid.NewGuid(); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs index 1c998ac7a..16197164d 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs @@ -5,16 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Infrastructure.Assets; - namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public sealed class UpdateAsset : AssetCommand + public sealed class UpdateAsset : UploadAssetCommand { - public AssetFile File { get; set; } - - public ImageInfo ImageInfo { get; set; } - - public string FileHash { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs new file mode 100644 index 000000000..5ef0652cd --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public abstract class UploadAssetCommand : AssetCommand + { + public AssetFile File { get; set; } + + public ImageInfo ImageInfo { get; set; } + + public string FileHash { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 2c57f06da..1a01b229a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -8,6 +8,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using GraphQL; using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; @@ -18,28 +19,14 @@ 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 IAssetQueryService assetQuery; - private readonly IAppProvider appProvider; - - public CachingGraphQLService( - IMemoryCache cache, - IAppProvider appProvider, - IAssetQueryService assetQuery, - IContentQueryService contentQuery, - IGraphQLUrlGenerator urlGenerator) + private readonly IDependencyResolver resolver; + + public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver) : base(cache) { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(urlGenerator, nameof(urlGenerator)); - - this.appProvider = appProvider; - this.assetQuery = assetQuery; - this.contentQuery = contentQuery; - this.urlGenerator = urlGenerator; + Guard.NotNull(resolver, nameof(resolver)); + + this.resolver = resolver; } public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries) @@ -49,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var model = await GetModelAsync(context.App); - var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator); + var ctx = new GraphQLExecutionContext(context, resolver); var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); @@ -63,14 +50,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var model = await GetModelAsync(context.App); - var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator); + var ctx = new GraphQLExecutionContext(context, resolver); var result = await QueryInternalAsync(model, ctx, query); return result; } - private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) + private async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) { if (string.IsNullOrWhiteSpace(query.Query)) { @@ -97,9 +84,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { entry.AbsoluteExpirationRelativeToNow = CacheDuration; - var allSchemas = await appProvider.GetSchemasAsync(app.Id); + var allSchemas = await resolver.Resolve().GetSchemasAsync(app.Id); - return new GraphQLModel(app, allSchemas, contentQuery.DefaultPageSizeGraphQl, assetQuery.DefaultPageSizeGraphQl, urlGenerator); + return new GraphQLModel(app, + allSchemas, + resolver.Resolve().DefaultPageSizeGraphQl, + resolver.Resolve().DefaultPageSizeGraphQl, + resolver.Resolve()); }); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 54a2793ab..c5dbf6024 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -7,37 +7,114 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using GraphQL; +using GraphQL.DataLoader; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public sealed class GraphQLExecutionContext : QueryExecutionContext { + private static readonly List EmptyAssets = new List(); + private static readonly List EmptyContents = new List(); + private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; + private readonly IDependencyResolver resolver; + public IGraphQLUrlGenerator UrlGenerator { get; } - public GraphQLExecutionContext(QueryContext context, - IAssetQueryService assetQuery, - IContentQueryService contentQuery, - IGraphQLUrlGenerator urlGenerator) - : base(context, assetQuery, contentQuery) + public ISemanticLog Log { get; } + + public GraphQLExecutionContext(QueryContext context, IDependencyResolver resolver) + : base(context, + resolver.Resolve(), + resolver.Resolve()) + { + UrlGenerator = resolver.Resolve(); + + dataLoaderContextAccessor = resolver.Resolve(); + + this.resolver = resolver; + } + + public void Setup(ExecutionOptions execution) + { + var loader = resolver.Resolve(); + + var logger = LoggingMiddleware.Create(resolver.Resolve()); + + execution.Listeners.Add(loader); + execution.FieldMiddleware.Use(logger); + + execution.UserContext = this; + } + + public override Task FindAssetAsync(Guid id) { - UrlGenerator = urlGenerator; + var dataLoader = GetAssetsLoader(); + + return dataLoader.LoadAsync(id); + } + + public override Task FindContentAsync(Guid schemaId, Guid id) + { + var dataLoader = GetContentsLoader(schemaId); + + return dataLoader.LoadAsync(id); } - public Task> GetReferencedAssetsAsync(IJsonValue value) + public async Task> GetReferencedAssetsAsync(IJsonValue value) { var ids = ParseIds(value); - return GetReferencedAssetsAsync(ids); + if (ids == null) + { + return EmptyAssets; + } + + var dataLoader = GetAssetsLoader(); + + return await dataLoader.LoadManyAsync(ids); } - public Task> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) + public async Task> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) { var ids = ParseIds(value); - return GetReferencedContentsAsync(schemaId, ids); + if (ids == null) + { + return EmptyContents; + } + + var dataLoader = GetContentsLoader(schemaId); + + return await dataLoader.LoadManyAsync(ids); + } + + private IDataLoader GetAssetsLoader() + { + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", + async batch => + { + var result = await GetReferencedAssetsAsync(new List(batch)); + + return result.ToDictionary(x => x.Id); + }); + } + + private IDataLoader GetContentsLoader(Guid schemaId) + { + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"Schema_{schemaId}", + async batch => + { + var result = await GetReferencedContentsAsync(schemaId, new List(batch)); + + return result.ToDictionary(x => x.Id); + }); } private static ICollection ParseIds(IJsonValue value) @@ -58,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } catch { - return new List(); + return null; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index a07e8aca3..5f4ee3be3 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -135,9 +135,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return partitionResolver(key); } - public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field) + public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) { - return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType)); + return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType, fieldName)); } public IGraphType GetAssetType() @@ -175,15 +175,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { Guard.NotNull(context, nameof(context)); - var inputs = query.Variables?.ToInputs(); - - var result = await new DocumentExecuter().ExecuteAsync(options => + var result = await new DocumentExecuter().ExecuteAsync(execution => { - options.OperationName = query.OperationName; - options.UserContext = context; - options.Schema = graphQLSchema; - options.Inputs = inputs; - options.Query = query.Query; + context.Setup(execution); + + execution.Schema = graphQLSchema; + execution.Inputs = query.Variables?.ToInputs(); + execution.Query = query.Query; }).ConfigureAwait(false); return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs index f44c36b58..1ee303d6a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs @@ -35,6 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL IGraphType GetContentDataType(Guid schemaId); - (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field); + (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs new file mode 100644 index 000000000..9db32cab0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Instrumentation; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public static class LoggingMiddleware + { + public static Func Create(ISemanticLog log) + { + Guard.NotNull(log, nameof(log)); + + return new Func(next => + { + return async context => + { + try + { + return await next(context); + } + catch (Exception ex) + { + log.LogWarning(ex, w => w + .WriteProperty("action", "reolveField") + .WriteProperty("status", "failed") + .WriteProperty("field", context.FieldName)); + + throw ex; + } + }; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index 7dfd1480b..eb6ef19f5 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) { - var (resolvedType, valueResolver) = model.GetGraphType(schema, field); + var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); if (valueResolver != null) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs index 9531680b0..5b78b2728 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using GraphQL.DataLoader; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; @@ -37,5 +39,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return value; } + + public static async Task> LoadManyAsync(this IDataLoader dataLoader, ICollection keys) where T : class + { + var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x))); + + return contents.Where(x => x != null).ToList(); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs index 7f64227bb..2964ed201 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs @@ -16,18 +16,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class NestedGraphType : ObjectGraphType { - public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field) + public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName) { var schemaType = schema.TypeName(); var schemaName = schema.DisplayName(); - var fieldName = field.DisplayName(); + var fieldDisplayName = field.DisplayName(); Name = $"{schemaType}{fieldName}ChildDto"; foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) { - var fieldInfo = model.GetGraphType(schema, nestedField); + var fieldInfo = model.GetGraphType(schema, nestedField, nestedName); if (fieldInfo.ResolveType != null) { @@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = nestedName, Resolver = resolver, ResolvedType = fieldInfo.ResolveType, - Description = $"The {fieldName}/{nestedField.DisplayName()} nested field." + Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." }); } } - Description = $"The structure of the {schemaName}.{fieldName} nested schema."; + Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema."; } private static FuncFieldResolver ValueResolver(NestedField nestedField, (IGraphType ResolveType, ValueResolver Resolver) fieldInfo) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs index 32a9a308f..195eee1a7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs @@ -22,13 +22,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private readonly Func schemaResolver; private readonly IGraphModel model; private readonly IGraphType assetListType; + private readonly string fieldName; - public QueryGraphTypeVisitor(ISchemaEntity schema, Func schemaResolver, IGraphModel model, IGraphType assetListType) + public QueryGraphTypeVisitor(ISchemaEntity schema, Func schemaResolver, IGraphModel model, IGraphType assetListType, string fieldName) { this.model = model; this.assetListType = assetListType; this.schema = schema; this.schemaResolver = schemaResolver; + this.fieldName = fieldName; } public (IGraphType ResolveType, ValueResolver Resolver) Visit(IArrayField field) @@ -93,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private (IGraphType ResolveType, ValueResolver Resolver) ResolveNested(IArrayField field) { - var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field))); + var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, this.fieldName))); return (schemaFieldType, NoopResolver); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs index 666b7474d..8b186cbd4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.context = context; } - public async Task FindAssetAsync(Guid id) + public virtual async Task FindAssetAsync(Guid id) { var asset = cachedAssets.GetOrDefault(id); @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return asset; } - public async Task FindContentAsync(Guid schemaId, Guid id) + public virtual async Task FindContentAsync(Guid schemaId, Guid id) { var content = cachedContents.GetOrDefault(id); @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return content; } - public async Task> QueryAssetsAsync(string query) + public virtual async Task> QueryAssetsAsync(string query) { var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query)); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return assets; } - public async Task> QueryContentsAsync(string schemaIdOrName, string query) + public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) { var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return result; } - public async Task> GetReferencedAssetsAsync(ICollection ids) + public virtual async Task> GetReferencedAssetsAsync(ICollection ids) { Guard.NotNull(ids, nameof(ids)); @@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); } - public async Task> GetReferencedContentsAsync(Guid schemaId, ICollection ids) + public virtual async Task> GetReferencedContentsAsync(Guid schemaId, ICollection ids) { Guard.NotNull(ids, nameof(ids)); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs index 9936e98ef..fe48b89e4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs @@ -22,17 +22,17 @@ namespace Squidex.Domain.Apps.Entities.Contents public Instant DueTime { get; } - public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant due) + public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime) { Id = id; ScheduledBy = scheduledBy; Status = status; - DueTime = due; + DueTime = dueTime; } - public static ScheduleJob Build(Status status, RefToken by, Instant due) + public static ScheduleJob Build(Status status, RefToken scheduledBy, Instant dueTime) { - return new ScheduleJob(Guid.NewGuid(), status, by, due); + return new ScheduleJob(Guid.NewGuid(), status, scheduledBy, dueTime); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Q.cs b/src/Squidex.Domain.Apps.Entities/Q.cs index 8bd9b0f39..966bdd279 100644 --- a/src/Squidex.Domain.Apps.Entities/Q.cs +++ b/src/Squidex.Domain.Apps.Entities/Q.cs @@ -25,6 +25,11 @@ namespace Squidex.Domain.Apps.Entities return Clone(c => c.ODataQuery = odataQuery); } + public Q WithIds(params Guid[] ids) + { + return Clone(c => c.Ids = ids.ToList()); + } + public Q WithIds(IEnumerable ids) { return Clone(c => c.Ids = ids.ToList()); diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index 76be0fd06..cfe546a24 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -13,6 +13,14 @@ namespace Squidex.Infrastructure { public static class CollectionExtensions { + public static void AddRange(this ICollection target, IEnumerable source) + { + foreach (var value in source) + { + target.Add(value); + } + } + public static IEnumerable Shuffle(this IEnumerable enumerable) { var random = new Random(); diff --git a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs index f3b32121f..050a7619c 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs @@ -14,6 +14,7 @@ using NSwag.SwaggerGeneration; using NSwag.SwaggerGeneration.Processors; using Squidex.Areas.Api.Controllers.Contents.Generator; using Squidex.Areas.Api.Controllers.Rules.Models; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; namespace Squidex.Areas.Api.Config.Swagger @@ -76,7 +77,8 @@ namespace Squidex.Areas.Api.Config.Swagger }), new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String), - new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String) + new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String), + new PrimitiveTypeMapper(typeof(Status), s => s.Type = JsonObjectType.String), }; } } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 8f0f95fa2..965056fbe 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -182,7 +182,7 @@ namespace Squidex.Areas.Api.Controllers.Assets var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = AssetDto.FromAsset(result.Asset, this, app, result.IsDuplicate); + var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags, result.IsDuplicate); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); } @@ -267,8 +267,8 @@ namespace Squidex.Areas.Api.Controllers.Assets { var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = AssetDto.FromAsset(result, this, app); + var result = context.Result(); + var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index f44c9d25a..ec0e68899 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -118,10 +118,15 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [JsonProperty("_meta")] public AssetMetadata Metadata { get; set; } - public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, bool isDuplicate = false) + public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, HashSet tags = null, bool isDuplicate = false) { var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); + if (tags != null) + { + response.Tags = tags; + } + if (isDuplicate) { response.Metadata = new AssetMetadata { IsDuplicate = "true" }; diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs index b0e7b34cb..ef7ebda7b 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; @@ -25,13 +26,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public Status Status { get; set; } /// - /// The user who schedule the content. + /// The target date and time when the content should be scheduled. /// - public RefToken ScheduledBy { get; set; } + public Instant DueTime { get; set; } /// - /// The target date and time when the content should be scheduled. + /// The user who schedule the content. /// - public Instant DueTime { get; set; } + [Required] + public RefToken ScheduledBy { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index 201e3dadd..a513495ab 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -80,7 +80,10 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models AddPutLink("update", controller.Url(x => nameof(x.PutEvent), values)); - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteEvent), values)); + if (NextAttempt.HasValue) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteEvent), values)); + } return this; } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 55e68584b..6c50117dc 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -513,9 +513,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas [ApiCosts(1)] public async Task DeleteNestedField(string app, string name, long parentId, long id) { - await CommandBus.PublishAsync(new DeleteField { ParentFieldId = parentId, FieldId = id }); + var command = new DeleteField { ParentFieldId = parentId, FieldId = id }; - return NoContent(); + var response = await InvokeCommandAsync(app, command); + + return Ok(response); } private async Task InvokeCommandAsync(string app, ICommand command) diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml b/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml index 54b6042d0..dd23d6a95 100644 --- a/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml +++ b/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml @@ -53,22 +53,22 @@ { if (Model.IsLogin) { - if (Model.IsFailed) - { -
Email or password not correct.
- } + if (Model.IsFailed) + { +
Email or password not correct.
+ } -
-
- -
+ +
+ +
-
- -
+
+ +
- -
+ + } else { @@ -95,7 +95,6 @@ else var redirectButtons = document.getElementsByClassName("redirect-button"); if (redirectButtons.length === 1) { - debugger; redirectButtons[0].click(); } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index d44e12150..d6965d669 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -6,6 +6,8 @@ // ========================================================================== using System; +using GraphQL; +using GraphQL.DataLoader; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -74,6 +76,18 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As(); + services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t))) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html index f1ae6a0f7..da925bae6 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html @@ -22,13 +22,13 @@ - @@ -38,12 +38,12 @@
-
- +
diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts index f169a8ff6..a3e1f140b 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts @@ -29,9 +29,6 @@ export class ContentChangedTriggerComponent implements OnInit { @Input() public schemas: ImmutableArray; - @Input() - public isEditable: boolean; - @Input() public trigger: any; diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html index 1b4866640..cf9681be6 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html @@ -65,6 +65,7 @@
{ this.analytics.trackEvent('Analytics', 'Updated', appName); }), - pretifyError('Failed to delete asset. Please reload.')); + pretifyError('Failed to update asset. Please reload.')); } public deleteAsset(appName: string, asset: Resource, version: Version): Observable> { diff --git a/src/Squidex/app/shared/services/rules.service.spec.ts b/src/Squidex/app/shared/services/rules.service.spec.ts index 399580e53..99c00490a 100644 --- a/src/Squidex/app/shared/services/rules.service.spec.ts +++ b/src/Squidex/app/shared/services/rules.service.spec.ts @@ -288,41 +288,15 @@ describe('RulesService', () => { req.flush({ total: 20, items: [ - { - id: 'id1', - created: '2017-12-12T10:10', - eventName: 'event1', - nextAttempt: '2017-12-12T12:10', - jobResult: 'Failed', - lastDump: 'dump1', - numCalls: 1, - description: 'url1', - result: 'Failed' - }, - { - id: 'id2', - created: '2017-12-13T10:10', - eventName: 'event2', - nextAttempt: '2017-12-13T12:10', - jobResult: 'Failed', - lastDump: 'dump2', - numCalls: 2, - description: 'url2', - result: 'Failed' - } + ruleEventResponse(1), + ruleEventResponse(2) ] }); expect(rules!).toEqual( new RuleEventsDto(20, [ - new RuleEventDto('id1', - DateTime.parseISO_UTC('2017-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T12:10'), - 'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), - new RuleEventDto('id2', - DateTime.parseISO_UTC('2017-12-13T10:10'), - DateTime.parseISO_UTC('2017-12-13T12:10'), - 'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) + createRuleEvent(1), + createRuleEvent(2) ])); })); @@ -364,6 +338,23 @@ describe('RulesService', () => { req.flush({}); })); + function ruleEventResponse(id: number, suffix = '') { + return { + id: `id${id}`, + created: `${id % 1000 + 2000}-12-12T10:10:00`, + eventName: `event${id}${suffix}`, + nextAttempt: `${id % 1000 + 2000}-11-11T10:10`, + jobResult: `Failed${id}${suffix}`, + lastDump: `dump${id}${suffix}`, + numCalls: id, + description: `url${id}${suffix}`, + result: `Failed${id}${suffix}`, + _links: { + update: { method: 'PUT', href: `/rules/events/${id}` } + } + }; + } + function ruleResponse(id: number, suffix = '') { return { id: `id${id}`, @@ -390,6 +381,22 @@ describe('RulesService', () => { } }); +export function createRuleEvent(id: number, suffix = '') { + const links: ResourceLinks = { + update: { method: 'PUT', href: `/rules/events/${id}` } + }; + + return new RuleEventDto(links, `id${id}`, + DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), + DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), + `event${id}${suffix}`, + `url${id}${suffix}`, + `dump${id}${suffix}`, + `Failed${id}${suffix}`, + `Failed${id}${suffix}`, + id); +} + export function createRule(id: number, suffix = '') { const links: ResourceLinks = { update: { method: 'PUT', href: `/rules/${id}` } diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts index bd44ae278..5c42f7dec 100644 --- a/src/Squidex/app/shared/services/rules.service.ts +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -127,7 +127,10 @@ export class RuleEventsDto extends ResultSet { export class RuleEventDto extends Model { public readonly _links: ResourceLinks; - constructor( + public readonly canDelete: boolean; + public readonly canUpdate: boolean; + + constructor(links: ResourceLinks, public readonly id: string, public readonly created: DateTime, public readonly nextAttempt: DateTime | null, @@ -139,6 +142,11 @@ export class RuleEventDto extends Model { public readonly numCalls: number ) { super(); + + this._links = links; + + this.canDelete = hasAnyLink(links, 'delete'); + this.canUpdate = hasAnyLink(links, 'update'); } } @@ -287,7 +295,7 @@ export class RulesService { const items: any[] = body.items; const ruleEvents = new RuleEventsDto(body.total, items.map(item => - new RuleEventDto( + new RuleEventDto(item._links, item.id, DateTime.parseISO_UTC(item.created), item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index e02391b2a..d2f6f7517 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -213,7 +213,7 @@ export class SchemaPropertiesDto { export interface AddFieldDto { readonly name: string; - readonly partitioning: string; + readonly partitioning?: string; readonly properties: FieldPropertiesDto; } diff --git a/src/Squidex/app/shared/state/rule-events.state.spec.ts b/src/Squidex/app/shared/state/rule-events.state.spec.ts index 5adcd6f4a..cd0ebcab6 100644 --- a/src/Squidex/app/shared/state/rule-events.state.spec.ts +++ b/src/Squidex/app/shared/state/rule-events.state.spec.ts @@ -9,14 +9,14 @@ import { of } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; import { - DateTime, DialogService, - RuleEventDto, RuleEventsDto, RuleEventsState, RulesService } from '@app/shared/internal'; +import { createRuleEvent } from '../services/rules.service.spec'; + import { TestValues } from './_test-helpers'; describe('RuleEventsState', () => { @@ -26,8 +26,8 @@ describe('RuleEventsState', () => { } = TestValues; const oldRuleEvents = [ - new RuleEventDto('id1', DateTime.now(), null, 'event1', 'description', 'dump1', 'result1', 'result1', 1), - new RuleEventDto('id2', DateTime.now(), null, 'event2', 'description', 'dump2', 'result2', 'result2', 2) + createRuleEvent(1), + createRuleEvent(2) ]; let dialogs: IMock; diff --git a/src/Squidex/app/shared/state/schemas.state.spec.ts b/src/Squidex/app/shared/state/schemas.state.spec.ts index 9fe337b67..ed196f16c 100644 --- a/src/Squidex/app/shared/state/schemas.state.spec.ts +++ b/src/Squidex/app/shared/state/schemas.state.spec.ts @@ -13,6 +13,7 @@ import { SchemaCategory, SchemasState } from './schemas.state'; import { DialogService, + FieldDto, ImmutableArray, SchemaDetailsDto, SchemasService, @@ -366,28 +367,38 @@ describe('SchemasState', () => { schemasService.setup(x => x.postField(app, schema1, It.isAny(), version)) .returns(() => of(updated)).verifiable(); - schemasState.addField(schema1, request).subscribe(); + let newField: FieldDto; + + schemasState.addField(schema1, request).subscribe(result => { + newField = result; + }); const schema1New = schemasState.snapshot.schemas.at(0); expect(schema1New).toEqual(updated); expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(newField!).toBeDefined(); }); it('should update schema and selected schema when nested field added', () => { - const request = { ...schema.fields[0] }; + const request = { ...schema.fields[0].nested[0] }; const updated = createSchemaDetails(1, newVersion, '-new'); schemasService.setup(x => x.postField(app, schema.fields[0], It.isAny(), version)) .returns(() => of(updated)).verifiable(); - schemasState.addField(schema1, request, schema.fields[0]).subscribe(); + let newField: FieldDto; + + schemasState.addField(schema1, request, schema.fields[0]).subscribe(result => { + newField = result; + }); const schema1New = schemasState.snapshot.schemas.at(0); expect(schema1New).toEqual(updated); expect(schemasState.snapshot.selectedSchema).toEqual(updated); + expect(newField!).toBeDefined(); }); it('should update schema and selected schema when field removed', () => { diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/src/Squidex/app/shared/state/schemas.state.ts index 87dea5d3c..34a44d4ba 100644 --- a/src/Squidex/app/shared/state/schemas.state.ts +++ b/src/Squidex/app/shared/state/schemas.state.ts @@ -315,7 +315,7 @@ export class SchemasState extends State { function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto): FieldDto { if (parent) { - return parent.nested.find(f => f.name === request.name)!; + return x.fields.find(f => f.fieldId === parent.fieldId)!.nested.find(f => f.name === request.name)!; } else { return x.fields.find(f => f.name === request.name)!; } diff --git a/src/Squidex/package-lock.json b/src/Squidex/package-lock.json index 3051cf82b..b8073c031 100644 --- a/src/Squidex/package-lock.json +++ b/src/Squidex/package-lock.json @@ -3196,14 +3196,22 @@ } }, "browserslist": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.5.tgz", - "integrity": "sha512-z9ZhGc3d9e/sJ9dIx5NFXkKoaiQTnrvrMsN3R1fGb1tkWWNSz12UewJn9TNxGo1l7J23h0MRaPmk7jfeTZYs1w==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.3.tgz", + "integrity": "sha512-CNBqTCq22RKM8wKJNowcqihHJ4SkI8CGeK7KOR9tPboXUuS5Zk5lQgzzTbs4oxD8x+6HUshZUa2OyNI9lR93bQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30000912", - "electron-to-chromium": "^1.3.86", - "node-releases": "^1.0.5" + "caniuse-lite": "^1.0.30000975", + "electron-to-chromium": "^1.3.164", + "node-releases": "^1.1.23" + }, + "dependencies": { + "caniuse-lite": { + "version": "1.0.30000975", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000975.tgz", + "integrity": "sha512-ZsXA9YWQX6ATu5MNg+Vx/cMQ+hM6vBBSqDeJs8ruk9z0ky4yIHML15MoxcFt088ST2uyjgqyUGRJButkptWf0w==", + "dev": true + } } }, "buffer": { @@ -3450,9 +3458,9 @@ } }, "caniuse-lite": { - "version": "1.0.30000918", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000918.tgz", - "integrity": "sha512-CAZ9QXGViBvhHnmIHhsTPSWFBujDaelKnUj7wwImbyQRxmXynYqKGi3UaZTSz9MoVh+1EVxOS/DFIkrJYgR3aw==", + "version": "1.0.30000975", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000975.tgz", + "integrity": "sha512-ZsXA9YWQX6ATu5MNg+Vx/cMQ+hM6vBBSqDeJs8ruk9z0ky4yIHML15MoxcFt088ST2uyjgqyUGRJButkptWf0w==", "dev": true }, "canonical-path": { @@ -4950,9 +4958,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.90", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.90.tgz", - "integrity": "sha512-IjJZKRhFbWSOX1w0sdIXgp4CMRguu6UYcTckyFF/Gjtemsu/25eZ+RXwFlV+UWcIueHyQA1UnRJxocTpH5NdGA==", + "version": "1.3.164", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.164.tgz", + "integrity": "sha512-VLlalqUeduN4+fayVtRZvGP2Hl1WrRxlwzh2XVVMJym3IFrQUS29BFQ1GP/BxOJXJI1OFCrJ5BnFEsAe8NHtOg==", "dev": true }, "elliptic": { @@ -10755,9 +10763,9 @@ } }, "node-releases": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.1.tgz", - "integrity": "sha512-2UXrBr6gvaebo5TNF84C66qyJJ6r0kxBObgZIDX3D3/mt1ADKiHux3NJPWisq0wxvJJdkjECH+9IIKYViKj71Q==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.23.tgz", + "integrity": "sha512-uq1iL79YjfYC0WXoHbC/z28q/9pOl8kSHaXdWmAAc8No+bDwqkZbzIJz55g/MUsPgSGm9LZ7QSUbzTcH5tz47w==", "dev": true, "requires": { "semver": "^5.3.0" diff --git a/src/Squidex/package.json b/src/Squidex/package.json index 72f1ab671..09a66cd63 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -47,21 +47,23 @@ "zone.js": "0.9.1" }, "devDependencies": { - "@angular/compiler-cli": "7.2.14", "@angular/compiler": "7.2.14", + "@angular/compiler-cli": "7.2.14", "@ngtools/webpack": "7.3.8", "@types/core-js": "2.5.0", "@types/jasmine": "3.3.12", "@types/marked": "0.6.5", "@types/mousetrap": "1.6", "@types/node": "12.0.0", - "@types/react-dom": "16.8.4", "@types/react": "16.8.16", + "@types/react-dom": "16.8.4", "@types/sortablejs": "1.7.2", "angular-router-loader": "0.8.5", "angular2-template-loader": "0.6.2", "awesome-typescript-loader": "5.2.1", "babel-core": "6.26.3", + "browserslist": "^4.6.3", + "caniuse-lite": "^1.0.30000975", "circular-dependency-plugin": "5.0.2", "codelyzer": "5.0.1", "cpx": "1.5.0", @@ -72,16 +74,16 @@ "ignore-loader": "0.1.2", "istanbul-instrumenter-loader": "3.0.1", "jasmine-core": "3.4.0", + "karma": "4.1.0", "karma-chrome-launcher": "2.2.0", "karma-cli": "2.0.0", "karma-coverage-istanbul-reporter": "2.0.5", "karma-htmlfile-reporter": "0.3.8", - "karma-jasmine-html-reporter": "1.4.2", "karma-jasmine": "2.0.1", + "karma-jasmine-html-reporter": "1.4.2", "karma-mocha-reporter": "2.2.5", "karma-sourcemap-loader": "0.3.7", "karma-webpack": "3.0.5", - "karma": "4.1.0", "mini-css-extract-plugin": "0.6.0", "node-sass": "4.12.0", "optimize-css-assets-webpack-plugin": "5.0.1", @@ -93,15 +95,15 @@ "style-loader": "0.23.1", "ts-loader": "5.4.5", "tsconfig-paths-webpack-plugin": "3.2.0", - "tslint-webpack-plugin": "2.0.4", "tslint": "5.16.0", + "tslint-webpack-plugin": "2.0.4", "typemoq": "2.1.0", "typescript": "3.2.4", "uglifyjs-webpack-plugin": "2.1.2", "underscore": "1.9.1", + "webpack": "4.30.0", "webpack-cli": "3.3.1", "webpack-dev-server": "3.3.1", - "webpack-merge": "4.2.1", - "webpack": "4.30.0" + "webpack-merge": "4.2.1" } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 21df10911..00f6856ed 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -54,19 +54,23 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A.Ignored)) .Returns(new List()); - A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>.Ignored, A>.Ignored)) - .Returns(new Dictionary()); + A.CallTo(() => tagService.DenormalizeTagsAsync(AppId, TagGroups.Assets, A>.Ignored)) + .Returns(new Dictionary + { + ["1"] = "foundTag1", + ["2"] = "foundTag2" + }); A.CallTo(() => grainFactory.GetGrain(Id, null)) .Returns(asset); - sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }); + sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }, tagService); } [Fact] public async Task Create_should_create_domain_object() { - var command = new CreateAsset { AssetId = assetId, File = file }; + var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(command); SetupTags(command); @@ -80,6 +84,8 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Contains("tag1", command.Tags); Assert.Contains("tag2", command.Tags); + Assert.Equal(new HashSet { "tag1", "tag2" }, result.Tags); + AssertAssetHasBeenUploaded(0, context.ContextId); AssertAssetImageChecked(); } @@ -87,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Create_should_calculate_hash() { - var command = new CreateAsset { AssetId = assetId, File = file }; + var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(command); SetupImageInfo(); @@ -100,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() { - var command = new CreateAsset { AssetId = assetId, File = file }; + var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(command); SetupSameHashAsset(file.FileName, file.FileSize, out _); @@ -108,13 +114,15 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.HandleAsync(context); - Assert.True(context.Result().IsDuplicate); + var result = context.Result(); + + Assert.True(result.IsDuplicate); } [Fact] public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found() { - var command = new CreateAsset { AssetId = assetId, File = file }; + var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(command); SetupSameHashAsset("other-name", file.FileSize, out _); @@ -122,13 +130,31 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.HandleAsync(context); - Assert.False(context.Result().IsDuplicate); + var result = context.Result(); + + Assert.False(result.IsDuplicate); + } + + [Fact] + public async Task Create_should_resolve_tag_names_for_duplicate() + { + var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file }); + var context = CreateContextForCommand(command); + + SetupSameHashAsset(file.FileName, file.FileSize, out _); + SetupImageInfo(); + + await sut.HandleAsync(context); + + var result = context.Result(); + + Assert.Equal(new HashSet { "foundTag1", "foundTag2" }, result.Tags); } [Fact] public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found() { - var command = new CreateAsset { AssetId = assetId, File = file }; + var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(command); SetupSameHashAsset(file.FileName, 12345, out _); @@ -142,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Update_should_update_domain_object() { - var command = new UpdateAsset { AssetId = assetId, File = file }; + var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(command); SetupImageInfo(); @@ -158,7 +184,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Update_should_calculate_hash() { - var command = new UpdateAsset { AssetId = assetId, File = file }; + var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file }); var context = CreateContextForCommand(command); SetupImageInfo(); @@ -170,6 +196,40 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.True(command.FileHash.Length > 10); } + [Fact] + public async Task Update_should_resolve_tags() + { + var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file }); + var context = CreateContextForCommand(command); + + SetupImageInfo(); + + await ExecuteCreateAsync(); + + await sut.HandleAsync(context); + + var result = context.Result(); + + Assert.Equal(new HashSet { "foundTag1", "foundTag2" }, result.Tags); + } + + [Fact] + public async Task AnnotateAsset_should_resolve_tags() + { + var command = CreateCommand(new AnnotateAsset { AssetId = assetId, FileName = "newName" }); + var context = CreateContextForCommand(command); + + SetupImageInfo(); + + await ExecuteCreateAsync(); + + await sut.HandleAsync(context); + + var result = context.Result(); + + Assert.Equal(new HashSet { "foundTag1", "foundTag2" }, result.Tags); + } + private Task ExecuteCreateAsync() { return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file })); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 09b5afd92..2e503ffdf 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -212,8 +212,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()); - A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId)) - .Returns(asset); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId))) + .Returns(ResultList.Create(1, asset)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -544,8 +544,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) - .Returns(content); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId))) + .Returns(ResultList.Create(1, content)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -635,8 +635,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) - .Returns(content); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId))) + .Returns(ResultList.Create(1, content)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -730,12 +730,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) - .Returns(content); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A.Ignored)) .Returns(ResultList.Create(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId))) + .Returns(ResultList.Create(1, content)); + var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var expected = new @@ -788,8 +788,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) - .Returns(content); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId))) + .Returns(ResultList.Create(1, content)); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) .Returns(ResultList.Create(0, assetRef)); @@ -844,10 +844,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId2.ToString()); - A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId1)) - .Returns(asset1); - A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId2)) - .Returns(asset2); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId1))) + .Returns(ResultList.Create(0, asset1)); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId2))) + .Returns(ResultList.Create(0, asset2)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); @@ -902,8 +903,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) - .Returns(content); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId))) + .Returns(ResultList.Create(1, content)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -940,8 +941,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) - .Returns(content); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId))) + .Returns(ResultList.Create(1, content)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -986,8 +987,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) - .Returns(content); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId))) + .Returns(ResultList.Create(1, content)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -1005,6 +1006,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); } + private static Q MatchId(Guid contentId) + { + return A.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId); + } + private QueryContext MatchsAssetContext() { return A.That.Matches(x => x.App == app && x.User == user); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index cfc9e817b..93dc8b070 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -9,6 +9,8 @@ using System; using System.Collections.Generic; using System.Security.Claims; using FakeItEasy; +using GraphQL; +using GraphQL.DataLoader; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -24,6 +26,7 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; using Xunit; #pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter @@ -33,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public class GraphQLTestBase { - protected readonly Schema schemaDef; protected readonly Guid schemaId = Guid.NewGuid(); protected readonly Guid appId = Guid.NewGuid(); protected readonly string appName = "my-app"; @@ -41,8 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL protected readonly IAssetQueryService assetQuery = A.Fake(); protected readonly ISchemaEntity schema = A.Fake(); protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); - protected readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - protected readonly IAppProvider appProvider = A.Fake(); + protected readonly IDependencyResolver dependencyResolver; protected readonly IAppEntity app = A.Dummy(); protected readonly QueryContext context; protected readonly ClaimsPrincipal user = new ClaimsPrincipal(); @@ -50,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public GraphQLTestBase() { - schemaDef = + var schemaDef = new Schema("my-schema") .AddJson(1, "my-json", Partitioning.Invariant, new JsonFieldProperties()) @@ -92,11 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => schema.Id).Returns(schemaId); A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - var allSchemas = new List { schema }; - - A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas); - - sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator()); + sut = CreateSut(); } protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null) @@ -210,5 +207,32 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { return serializer.Serialize(result); } + + private CachingGraphQLService CreateSut() + { + var appProvider = A.Fake(); + + A.CallTo(() => appProvider.GetSchemasAsync(appId)) + .Returns(new List { schema }); + + var dataLoaderContext = new DataLoaderContextAccessor(); + + var services = new Dictionary + { + [typeof(IAppProvider)] = appProvider, + [typeof(IAssetQueryService)] = assetQuery, + [typeof(IContentQueryService)] = contentQuery, + [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, + [typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(), + [typeof(ISemanticLog)] = A.Fake(), + [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext) + }; + + var resolver = new FuncDependencyResolver(t => services[t]); + + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + return new CachingGraphQLService(cache, resolver); + } } }