Browse Source

Merge branch 'next' into refactoring/schema-categories

# Conflicts:
#	src/Squidex/app/shared/components/schema-category.component.ts
#	src/Squidex/app/shared/state/schemas.state.spec.ts
pull/375/head
Sebastian Stehle 7 years ago
parent
commit
5aa71dfa59
  1. 6
      .testrunsettings
  2. 28
      CHANGELOG.md
  3. 12
      Dockerfile
  4. 12
      Dockerfile.build
  5. 7
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  6. 9
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  7. 64
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  8. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  9. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs
  10. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  11. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  12. 20
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  13. 41
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  14. 99
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  15. 18
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  16. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  17. 42
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs
  18. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  19. 9
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs
  20. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  21. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  22. 12
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  23. 8
      src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs
  24. 5
      src/Squidex.Domain.Apps.Entities/Q.cs
  25. 8
      src/Squidex.Infrastructure/CollectionExtensions.cs
  26. 4
      src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
  27. 6
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  28. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  29. 10
      src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs
  30. 3
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs
  31. 6
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  32. 1
      src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
  33. 14
      src/Squidex/Config/Domain/EntitiesServices.cs
  34. 8
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  35. 3
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  36. 1
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  37. 2
      src/Squidex/app/shared/services/assets.service.ts
  38. 67
      src/Squidex/app/shared/services/rules.service.spec.ts
  39. 12
      src/Squidex/app/shared/services/rules.service.ts
  40. 2
      src/Squidex/app/shared/services/schemas.service.ts
  41. 8
      src/Squidex/app/shared/state/rule-events.state.spec.ts
  42. 17
      src/Squidex/app/shared/state/schemas.state.spec.ts
  43. 2
      src/Squidex/app/shared/state/schemas.state.ts
  44. 38
      src/Squidex/package-lock.json
  45. 16
      src/Squidex/package.json
  46. 84
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  47. 48
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  48. 42
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

6
.testrunsettings

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<RunConfiguration>
<MaxCpuCount>4</MaxCpuCount>
</RunConfiguration>
</RunSettings>

28
CHANGELOG.md

@ -1,5 +1,33 @@
# Changelog # 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 ## v2.0.5 - 2019-04-21
## Features ## Features

12
Dockerfile

@ -5,11 +5,20 @@ FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
WORKDIR /src WORKDIR /src
# Copy Node project files.
COPY src/Squidex/package*.json /tmp/ COPY src/Squidex/package*.json /tmp/
# Install Node packages # Install Node packages
RUN cd /tmp && npm install --loglevel=error 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 . . COPY . .
# Build Frontend # Build Frontend
@ -19,8 +28,7 @@ RUN cp -a /tmp/node_modules src/Squidex/ \
&& npm run build && npm run build
# Test Backend # Test Backend
RUN dotnet restore \ RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \
&& dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ && 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.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \

12
Dockerfile.build

@ -2,11 +2,20 @@ FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
WORKDIR /src WORKDIR /src
# Copy Node project files.
COPY src/Squidex/package*.json /tmp/ COPY src/Squidex/package*.json /tmp/
# Install Node packages # Install Node packages
RUN cd /tmp && npm install --loglevel=error 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 . . COPY . .
# Build Frontend # Build Frontend
@ -16,8 +25,7 @@ RUN cp -a /tmp/node_modules src/Squidex/ \
&& npm run build && npm run build
# Test Backend # Test Backend
RUN dotnet restore \ RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \
&& dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ && 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.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \

7
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 find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified);
var assetItems = find.ToListAsync(); var assetItems = await find.ToListAsync();
var assetCount = find.CountDocumentsAsync();
await Task.WhenAll(assetItems, assetCount); return ResultList.Create(assetItems.Count, assetItems.OfType<IAssetEntity>());
return ResultList.Create(assetCount.Result, assetItems.Result.OfType<IAssetEntity>());
} }
} }

9
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 find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status));
var contentItems = find.WithoutDraft(includeDraft).ToListAsync(); var contentItems = await find.WithoutDraft(includeDraft).ToListAsync();
var contentCount = find.CountDocumentsAsync();
await Task.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result) foreach (var entity in contentItems)
{ {
entity.ParseData(schema.SchemaDef, serializer); entity.ParseData(schema.SchemaDef, serializer);
} }
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result); return ResultList.Create<IContentEntity>(contentItems.Count, contentItems);
} }
public async Task<IContentEntity> FindContentAsync(ISchemaEntity schema, Guid id, Status[] status, bool includeDraft) public async Task<IContentEntity> FindContentAsync(ISchemaEntity schema, Guid id, Status[] status, bool includeDraft)

64
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -24,25 +25,28 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators; private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware( public AssetCommandMiddleware(
IGrainFactory grainFactory, IGrainFactory grainFactory,
IAssetQueryService assetQuery, IAssetQueryService assetQuery,
IAssetStore assetStore, IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators) IEnumerable<ITagGenerator<CreateAsset>> tagGenerators,
ITagService tagService)
: base(grainFactory) : base(grainFactory)
{ {
Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators)); Guard.NotNull(tagGenerators, nameof(tagGenerators));
Guard.NotNull(tagService, nameof(tagService));
this.assetStore = assetStore; this.assetStore = assetStore;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators; this.tagGenerators = tagGenerators;
this.tagService = tagService;
} }
public override async Task HandleAsync(CommandContext context, Func<Task> next) public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -56,9 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.Tags = new HashSet<string>(); createAsset.Tags = new HashSet<string>();
} }
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead()); await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, context);
createAsset.FileHash = await UploadAsync(context, createAsset.File);
try try
{ {
@ -70,7 +73,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
if (IsDuplicate(createAsset, existing)) 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<string>(denormalizedTags.Values));
} }
break; break;
@ -85,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset); 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); 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: 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 try
{ {
var result = (IAssetEntity)await ExecuteCommandAsync(updateAsset); var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset);
context.Complete(result); 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 finally
{ {
@ -121,29 +126,54 @@ namespace Squidex.Domain.Apps.Entities.Assets
break; break;
} }
case AssetCommand command:
{
var result = await ExecuteAndAdjustTagsAsync(command);
context.Complete(result);
break;
}
default: default:
await base.HandleAsync(context, next); await base.HandleAsync(context, next);
break; break;
} }
} }
private async Task<object> 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<string>(denormalizedTags.Values));
}
return result;
}
private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset) private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset)
{ {
return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize; return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize;
} }
private async Task<string> 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); 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;
} }
} }
} }

11
src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs

@ -5,18 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetCreatedResult public sealed class AssetCreatedResult : AssetResult
{ {
public IAssetEntity Asset { get; }
public bool IsDuplicate { get; } public bool IsDuplicate { get; }
public AssetCreatedResult(IAssetEntity asset, bool isDuplicate) public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet<string> tags)
: base(asset, tags)
{ {
Asset = asset;
IsDuplicate = isDuplicate; IsDuplicate = isDuplicate;
} }
} }

25
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<string> Tags { get; }
public AssetResult(IAssetEntity asset, HashSet<string> tags)
{
Asset = asset;
Tags = tags;
}
}
}

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -8,22 +8,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class CreateAsset : AssetCommand, IAppCommand public sealed class CreateAsset : UploadAssetCommand, IAppCommand
{ {
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset() public CreateAsset()
{ {
AssetId = Guid.NewGuid(); AssetId = Guid.NewGuid();

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs

@ -5,16 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands 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; }
} }
} }

20
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; }
}
}

41
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using GraphQL;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
@ -18,28 +19,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService
{ {
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery; private readonly IDependencyResolver resolver;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetQueryService assetQuery; public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver)
private readonly IAppProvider appProvider;
public CachingGraphQLService(
IMemoryCache cache,
IAppProvider appProvider,
IAssetQueryService assetQuery,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(cache) : base(cache)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(resolver, nameof(resolver));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery)); this.resolver = resolver;
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider;
this.assetQuery = assetQuery;
this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator;
} }
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries) 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 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))); 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 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); var result = await QueryInternalAsync(model, ctx, query);
return result; 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)) if (string.IsNullOrWhiteSpace(query.Query))
{ {
@ -97,9 +84,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
entry.AbsoluteExpirationRelativeToNow = CacheDuration; entry.AbsoluteExpirationRelativeToNow = CacheDuration;
var allSchemas = await appProvider.GetSchemasAsync(app.Id); var allSchemas = await resolver.Resolve<IAppProvider>().GetSchemasAsync(app.Id);
return new GraphQLModel(app, allSchemas, contentQuery.DefaultPageSizeGraphQl, assetQuery.DefaultPageSizeGraphQl, urlGenerator); return new GraphQLModel(app,
allSchemas,
resolver.Resolve<IContentQueryService>().DefaultPageSizeGraphQl,
resolver.Resolve<IAssetQueryService>().DefaultPageSizeGraphQl,
resolver.Resolve<IGraphQLUrlGenerator>());
}); });
} }

99
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -7,37 +7,114 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using GraphQL;
using GraphQL.DataLoader;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class GraphQLExecutionContext : QueryExecutionContext public sealed class GraphQLExecutionContext : QueryExecutionContext
{ {
private static readonly List<IAssetEntity> EmptyAssets = new List<IAssetEntity>();
private static readonly List<IContentEntity> EmptyContents = new List<IContentEntity>();
private readonly IDataLoaderContextAccessor dataLoaderContextAccessor;
private readonly IDependencyResolver resolver;
public IGraphQLUrlGenerator UrlGenerator { get; } public IGraphQLUrlGenerator UrlGenerator { get; }
public GraphQLExecutionContext(QueryContext context, public ISemanticLog Log { get; }
IAssetQueryService assetQuery,
IContentQueryService contentQuery, public GraphQLExecutionContext(QueryContext context, IDependencyResolver resolver)
IGraphQLUrlGenerator urlGenerator) : base(context,
: base(context, assetQuery, contentQuery) resolver.Resolve<IAssetQueryService>(),
resolver.Resolve<IContentQueryService>())
{
UrlGenerator = resolver.Resolve<IGraphQLUrlGenerator>();
dataLoaderContextAccessor = resolver.Resolve<IDataLoaderContextAccessor>();
this.resolver = resolver;
}
public void Setup(ExecutionOptions execution)
{
var loader = resolver.Resolve<DataLoaderDocumentListener>();
var logger = LoggingMiddleware.Create(resolver.Resolve<ISemanticLog>());
execution.Listeners.Add(loader);
execution.FieldMiddleware.Use(logger);
execution.UserContext = this;
}
public override Task<IAssetEntity> FindAssetAsync(Guid id)
{ {
UrlGenerator = urlGenerator; var dataLoader = GetAssetsLoader();
return dataLoader.LoadAsync(id);
}
public override Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id)
{
var dataLoader = GetContentsLoader(schemaId);
return dataLoader.LoadAsync(id);
} }
public Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(IJsonValue value) public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
{ {
var ids = ParseIds(value); var ids = ParseIds(value);
return GetReferencedAssetsAsync(ids); if (ids == null)
{
return EmptyAssets;
} }
public Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) var dataLoader = GetAssetsLoader();
return await dataLoader.LoadManyAsync(ids);
}
public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, IJsonValue value)
{ {
var ids = ParseIds(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<Guid, IAssetEntity> GetAssetsLoader()
{
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IAssetEntity>("Assets",
async batch =>
{
var result = await GetReferencedAssetsAsync(new List<Guid>(batch));
return result.ToDictionary(x => x.Id);
});
}
private IDataLoader<Guid, IContentEntity> GetContentsLoader(Guid schemaId)
{
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IContentEntity>($"Schema_{schemaId}",
async batch =>
{
var result = await GetReferencedContentsAsync(schemaId, new List<Guid>(batch));
return result.ToDictionary(x => x.Id);
});
} }
private static ICollection<Guid> ParseIds(IJsonValue value) private static ICollection<Guid> ParseIds(IJsonValue value)
@ -58,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
catch catch
{ {
return new List<Guid>(); return null;
} }
} }
} }

18
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -135,9 +135,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key); 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() public IGraphType GetAssetType()
@ -175,15 +175,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
var inputs = query.Variables?.ToInputs(); var result = await new DocumentExecuter().ExecuteAsync(execution =>
var result = await new DocumentExecuter().ExecuteAsync(options =>
{ {
options.OperationName = query.OperationName; context.Setup(execution);
options.UserContext = context;
options.Schema = graphQLSchema; execution.Schema = graphQLSchema;
options.Inputs = inputs; execution.Inputs = query.Variables?.ToInputs();
options.Query = query.Query; execution.Query = query.Query;
}).ConfigureAwait(false); }).ConfigureAwait(false);
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray());

2
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 GetContentDataType(Guid schemaId);
(IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field); (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName);
} }
} }

42
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<FieldMiddlewareDelegate, FieldMiddlewareDelegate> Create(ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
return new Func<FieldMiddlewareDelegate, FieldMiddlewareDelegate>(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;
}
};
});
}
}
}

2
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()) 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) if (valueResolver != null)
{ {

9
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs

@ -7,6 +7,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using GraphQL.DataLoader;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -37,5 +39,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return value; return value;
} }
public static async Task<IReadOnlyList<T>> LoadManyAsync<TKey, T>(this IDataLoader<TKey, T> dataLoader, ICollection<TKey> keys) where T : class
{
var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x)));
return contents.Where(x => x != null).ToList();
}
} }
} }

10
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<JsonObject> public sealed class NestedGraphType : ObjectGraphType<JsonObject>
{ {
public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field) public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName)
{ {
var schemaType = schema.TypeName(); var schemaType = schema.TypeName();
var schemaName = schema.DisplayName(); var schemaName = schema.DisplayName();
var fieldName = field.DisplayName(); var fieldDisplayName = field.DisplayName();
Name = $"{schemaType}{fieldName}ChildDto"; Name = $"{schemaType}{fieldName}ChildDto";
foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) 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) if (fieldInfo.ResolveType != null)
{ {
@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = nestedName, Name = nestedName,
Resolver = resolver, Resolver = resolver,
ResolvedType = fieldInfo.ResolveType, 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<object> ValueResolver(NestedField nestedField, (IGraphType ResolveType, ValueResolver Resolver) fieldInfo) private static FuncFieldResolver<object> ValueResolver(NestedField nestedField, (IGraphType ResolveType, ValueResolver Resolver) fieldInfo)

6
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<Guid, IGraphType> schemaResolver; private readonly Func<Guid, IGraphType> schemaResolver;
private readonly IGraphModel model; private readonly IGraphModel model;
private readonly IGraphType assetListType; private readonly IGraphType assetListType;
private readonly string fieldName;
public QueryGraphTypeVisitor(ISchemaEntity schema, Func<Guid, IGraphType> schemaResolver, IGraphModel model, IGraphType assetListType) public QueryGraphTypeVisitor(ISchemaEntity schema, Func<Guid, IGraphType> schemaResolver, IGraphModel model, IGraphType assetListType, string fieldName)
{ {
this.model = model; this.model = model;
this.assetListType = assetListType; this.assetListType = assetListType;
this.schema = schema; this.schema = schema;
this.schemaResolver = schemaResolver; this.schemaResolver = schemaResolver;
this.fieldName = fieldName;
} }
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IArrayField field) 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) 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); return (schemaFieldType, NoopResolver);
} }

12
src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.context = context; this.context = context;
} }
public async Task<IAssetEntity> FindAssetAsync(Guid id) public virtual async Task<IAssetEntity> FindAssetAsync(Guid id)
{ {
var asset = cachedAssets.GetOrDefault(id); var asset = cachedAssets.GetOrDefault(id);
@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return asset; return asset;
} }
public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id) public virtual async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id)
{ {
var content = cachedContents.GetOrDefault(id); var content = cachedContents.GetOrDefault(id);
@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return content; return content;
} }
public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query) public virtual async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
{ {
var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query)); var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query));
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return assets; return assets;
} }
public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query) public virtual async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
{ {
var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query));
@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return result; return result;
} }
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids) public virtual async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids)
{ {
Guard.NotNull(ids, nameof(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(); return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList();
} }
public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids) public virtual async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids)
{ {
Guard.NotNull(ids, nameof(ids)); Guard.NotNull(ids, nameof(ids));

8
src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs

@ -22,17 +22,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Instant DueTime { get; } 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; Id = id;
ScheduledBy = scheduledBy; ScheduledBy = scheduledBy;
Status = status; 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);
} }
} }
} }

5
src/Squidex.Domain.Apps.Entities/Q.cs

@ -25,6 +25,11 @@ namespace Squidex.Domain.Apps.Entities
return Clone(c => c.ODataQuery = odataQuery); return Clone(c => c.ODataQuery = odataQuery);
} }
public Q WithIds(params Guid[] ids)
{
return Clone(c => c.Ids = ids.ToList());
}
public Q WithIds(IEnumerable<Guid> ids) public Q WithIds(IEnumerable<Guid> ids)
{ {
return Clone(c => c.Ids = ids.ToList()); return Clone(c => c.Ids = ids.ToList());

8
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -13,6 +13,14 @@ namespace Squidex.Infrastructure
{ {
public static class CollectionExtensions public static class CollectionExtensions
{ {
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
{
foreach (var value in source)
{
target.Add(value);
}
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable) public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{ {
var random = new Random(); var random = new Random();

4
src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs

@ -14,6 +14,7 @@ using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors;
using Squidex.Areas.Api.Controllers.Contents.Generator; using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Config.Swagger 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(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),
}; };
} }
} }

6
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 context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetCreatedResult>(); var result = context.Result<AssetCreatedResult>();
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); 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 context = await CommandBus.PublishAsync(command);
var result = context.Result<IAssetEntity>(); var result = context.Result<AssetResult>();
var response = AssetDto.FromAsset(result, this, app); var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags);
return response; return response;
} }

7
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -118,10 +118,15 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[JsonProperty("_meta")] [JsonProperty("_meta")]
public AssetMetadata Metadata { get; set; } 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<string> tags = null, bool isDuplicate = false)
{ {
var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
if (tags != null)
{
response.Tags = tags;
}
if (isDuplicate) if (isDuplicate)
{ {
response.Metadata = new AssetMetadata { IsDuplicate = "true" }; response.Metadata = new AssetMetadata { IsDuplicate = "true" };

10
src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.ComponentModel.DataAnnotations;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -25,13 +26,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
public Status Status { get; set; } public Status Status { get; set; }
/// <summary> /// <summary>
/// The user who schedule the content. /// The target date and time when the content should be scheduled.
/// </summary> /// </summary>
public RefToken ScheduledBy { get; set; } public Instant DueTime { get; set; }
/// <summary> /// <summary>
/// The target date and time when the content should be scheduled. /// The user who schedule the content.
/// </summary> /// </summary>
public Instant DueTime { get; set; } [Required]
public RefToken ScheduledBy { get; set; }
} }
} }

3
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs

@ -80,7 +80,10 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutEvent), values)); AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutEvent), values));
if (NextAttempt.HasValue)
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values)); AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values));
}
return this; return this;
} }

6
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -513,9 +513,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> 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<SchemaDetailsDto> InvokeCommandAsync(string app, ICommand command) private async Task<SchemaDetailsDto> InvokeCommandAsync(string app, ICommand command)

1
src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml

@ -95,7 +95,6 @@ else
var redirectButtons = document.getElementsByClassName("redirect-button"); var redirectButtons = document.getElementsByClassName("redirect-button");
if (redirectButtons.length === 1) { if (redirectButtons.length === 1) {
debugger;
redirectButtons[0].click(); redirectButtons[0].click();
} }
</script> </script>

14
src/Squidex/Config/Domain/EntitiesServices.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using GraphQL;
using GraphQL.DataLoader;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -74,6 +76,18 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetUsageTracker>() services.AddSingletonAs<AssetUsageTracker>()
.As<IEventConsumer>().As<IAssetUsageTracker>(); .As<IEventConsumer>().As<IAssetUsageTracker>();
services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t)))
.As<IDependencyResolver>();
services.AddSingletonAs<DataLoaderContextAccessor>()
.As<IDataLoaderContextAccessor>();
services.AddSingletonAs<DataLoaderDocumentListener>()
.AsSelf();
services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>();
services.AddSingletonAs<CachingGraphQLService>() services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>(); .As<IGraphQLService>();

8
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -22,13 +22,13 @@
</td> </td>
<td class="text-center"> <td class="text-center">
<input type="text" class="form-control code" placeholder="Optional condition as javascript expression" <input type="text" class="form-control code" placeholder="Optional condition as javascript expression"
[disabled]="!isEditable" [disabled]="triggerForm.disabled"
[ngModelOptions]="{ updateOn: 'blur' }" [ngModelOptions]="{ updateOn: 'blur' }"
[ngModel]="triggerSchema.condition" [ngModel]="triggerSchema.condition"
(ngModelChange)="updateCondition(triggerSchema.schema, $event)" /> (ngModelChange)="updateCondition(triggerSchema.schema, $event)" />
</td> </td>
<td class="text-center"> <td class="text-center">
<button type="button" class="btn btn-text-secondary" (click)="removeSchema(triggerSchema)" [disabled]="!isEditable"> <button type="button" class="btn btn-text-secondary" (click)="removeSchema(triggerSchema)" [disabled]="triggerForm.disabled">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</td> </td>
@ -38,12 +38,12 @@
<div class="section" *ngIf="schemasToAdd.length > 0"> <div class="section" *ngIf="schemasToAdd.length > 0">
<form class="form-inline" (ngSubmit)="addSchema()"> <form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1"> <div class="form-group mr-1">
<select class="form-control schemas-control" [disabled]="!isEditable" [(ngModel)]="schemaToAdd" name="schema"> <select class="form-control schemas-control" [disabled]="triggerForm.disabled" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd; trackBy: trackBySchema" [ngValue]="schema">{{schema.displayName}}</option> <option *ngFor="let schema of schemasToAdd; trackBy: trackBySchema" [ngValue]="schema">{{schema.displayName}}</option>
</select> </select>
</div> </div>
<button type="submit" class="btn btn-success" [disabled]="!isEditable">Add Schema</button> <button type="submit" class="btn btn-success" [disabled]="triggerForm.disabled">Add Schema</button>
</form> </form>
</div> </div>

3
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -29,9 +29,6 @@ export class ContentChangedTriggerComponent implements OnInit {
@Input() @Input()
public schemas: ImmutableArray<SchemaDto>; public schemas: ImmutableArray<SchemaDto>;
@Input()
public isEditable: boolean;
@Input() @Input()
public trigger: any; public trigger: any;

1
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -65,6 +65,7 @@
<ng-template #notEditing> <ng-template #notEditing>
<form [formGroup]="editForm.form" class="edit-form" (ngSubmit)="save()"> <form [formGroup]="editForm.form" class="edit-form" (ngSubmit)="save()">
<sqx-field-form <sqx-field-form
[isEditable]="true"
[patterns]="patternsState.patterns | async" [patterns]="patternsState.patterns | async"
[editForm]="editForm.form" [editForm]="editForm.form"
[editFormSubmitted]="editForm.submitted | async" [editFormSubmitted]="editForm.submitted | async"

2
src/Squidex/app/shared/services/assets.service.ts

@ -242,7 +242,7 @@ export class AssetsService {
tap(() => { tap(() => {
this.analytics.trackEvent('Analytics', 'Updated', appName); 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<Versioned<any>> { public deleteAsset(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> {

67
src/Squidex/app/shared/services/rules.service.spec.ts

@ -288,41 +288,15 @@ describe('RulesService', () => {
req.flush({ req.flush({
total: 20, total: 20,
items: [ items: [
{ ruleEventResponse(1),
id: 'id1', ruleEventResponse(2)
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'
}
] ]
}); });
expect(rules!).toEqual( expect(rules!).toEqual(
new RuleEventsDto(20, [ new RuleEventsDto(20, [
new RuleEventDto('id1', createRuleEvent(1),
DateTime.parseISO_UTC('2017-12-12T10:10'), createRuleEvent(2)
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)
])); ]));
})); }));
@ -364,6 +338,23 @@ describe('RulesService', () => {
req.flush({}); 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 = '') { function ruleResponse(id: number, suffix = '') {
return { return {
id: `id${id}`, 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 = '') { export function createRule(id: number, suffix = '') {
const links: ResourceLinks = { const links: ResourceLinks = {
update: { method: 'PUT', href: `/rules/${id}` } update: { method: 'PUT', href: `/rules/${id}` }

12
src/Squidex/app/shared/services/rules.service.ts

@ -127,7 +127,10 @@ export class RuleEventsDto extends ResultSet<RuleEventDto> {
export class RuleEventDto extends Model<RuleEventDto> { export class RuleEventDto extends Model<RuleEventDto> {
public readonly _links: ResourceLinks; public readonly _links: ResourceLinks;
constructor( public readonly canDelete: boolean;
public readonly canUpdate: boolean;
constructor(links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly created: DateTime, public readonly created: DateTime,
public readonly nextAttempt: DateTime | null, public readonly nextAttempt: DateTime | null,
@ -139,6 +142,11 @@ export class RuleEventDto extends Model<RuleEventDto> {
public readonly numCalls: number public readonly numCalls: number
) { ) {
super(); 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 items: any[] = body.items;
const ruleEvents = new RuleEventsDto(body.total, items.map(item => const ruleEvents = new RuleEventsDto(body.total, items.map(item =>
new RuleEventDto( new RuleEventDto(item._links,
item.id, item.id,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null,

2
src/Squidex/app/shared/services/schemas.service.ts

@ -213,7 +213,7 @@ export class SchemaPropertiesDto {
export interface AddFieldDto { export interface AddFieldDto {
readonly name: string; readonly name: string;
readonly partitioning: string; readonly partitioning?: string;
readonly properties: FieldPropertiesDto; readonly properties: FieldPropertiesDto;
} }

8
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 { IMock, It, Mock, Times } from 'typemoq';
import { import {
DateTime,
DialogService, DialogService,
RuleEventDto,
RuleEventsDto, RuleEventsDto,
RuleEventsState, RuleEventsState,
RulesService RulesService
} from '@app/shared/internal'; } from '@app/shared/internal';
import { createRuleEvent } from '../services/rules.service.spec';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
describe('RuleEventsState', () => { describe('RuleEventsState', () => {
@ -26,8 +26,8 @@ describe('RuleEventsState', () => {
} = TestValues; } = TestValues;
const oldRuleEvents = [ const oldRuleEvents = [
new RuleEventDto('id1', DateTime.now(), null, 'event1', 'description', 'dump1', 'result1', 'result1', 1), createRuleEvent(1),
new RuleEventDto('id2', DateTime.now(), null, 'event2', 'description', 'dump2', 'result2', 'result2', 2) createRuleEvent(2)
]; ];
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;

17
src/Squidex/app/shared/state/schemas.state.spec.ts

@ -13,6 +13,7 @@ import { SchemaCategory, SchemasState } from './schemas.state';
import { import {
DialogService, DialogService,
FieldDto,
ImmutableArray, ImmutableArray,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
@ -366,28 +367,38 @@ describe('SchemasState', () => {
schemasService.setup(x => x.postField(app, schema1, It.isAny(), version)) schemasService.setup(x => x.postField(app, schema1, It.isAny(), version))
.returns(() => of(updated)).verifiable(); .returns(() => of(updated)).verifiable();
schemasState.addField(schema1, request).subscribe(); let newField: FieldDto;
schemasState.addField(schema1, request).subscribe(result => {
newField = result;
});
const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0); const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0);
expect(schema1New).toEqual(updated); expect(schema1New).toEqual(updated);
expect(schemasState.snapshot.selectedSchema).toEqual(updated); expect(schemasState.snapshot.selectedSchema).toEqual(updated);
expect(newField!).toBeDefined();
}); });
it('should update schema and selected schema when nested field added', () => { 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'); const updated = createSchemaDetails(1, newVersion, '-new');
schemasService.setup(x => x.postField(app, schema.fields[0], It.isAny(), version)) schemasService.setup(x => x.postField(app, schema.fields[0], It.isAny(), version))
.returns(() => of(updated)).verifiable(); .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 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0); const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0);
expect(schema1New).toEqual(updated); expect(schema1New).toEqual(updated);
expect(schemasState.snapshot.selectedSchema).toEqual(updated); expect(schemasState.snapshot.selectedSchema).toEqual(updated);
expect(newField!).toBeDefined();
}); });
it('should update schema and selected schema when field removed', () => { it('should update schema and selected schema when field removed', () => {

2
src/Squidex/app/shared/state/schemas.state.ts

@ -315,7 +315,7 @@ export class SchemasState extends State<Snapshot> {
function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto): FieldDto { function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto): FieldDto {
if (parent) { 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 { } else {
return x.fields.find(f => f.name === request.name)!; return x.fields.find(f => f.name === request.name)!;
} }

38
src/Squidex/package-lock.json

@ -3196,14 +3196,22 @@
} }
}, },
"browserslist": { "browserslist": {
"version": "4.3.5", "version": "4.6.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.5.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.3.tgz",
"integrity": "sha512-z9ZhGc3d9e/sJ9dIx5NFXkKoaiQTnrvrMsN3R1fGb1tkWWNSz12UewJn9TNxGo1l7J23h0MRaPmk7jfeTZYs1w==", "integrity": "sha512-CNBqTCq22RKM8wKJNowcqihHJ4SkI8CGeK7KOR9tPboXUuS5Zk5lQgzzTbs4oxD8x+6HUshZUa2OyNI9lR93bQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30000912", "caniuse-lite": "^1.0.30000975",
"electron-to-chromium": "^1.3.86", "electron-to-chromium": "^1.3.164",
"node-releases": "^1.0.5" "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": { "buffer": {
@ -3450,9 +3458,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30000918", "version": "1.0.30000975",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000918.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000975.tgz",
"integrity": "sha512-CAZ9QXGViBvhHnmIHhsTPSWFBujDaelKnUj7wwImbyQRxmXynYqKGi3UaZTSz9MoVh+1EVxOS/DFIkrJYgR3aw==", "integrity": "sha512-ZsXA9YWQX6ATu5MNg+Vx/cMQ+hM6vBBSqDeJs8ruk9z0ky4yIHML15MoxcFt088ST2uyjgqyUGRJButkptWf0w==",
"dev": true "dev": true
}, },
"canonical-path": { "canonical-path": {
@ -4950,9 +4958,9 @@
"dev": true "dev": true
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.90", "version": "1.3.164",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.90.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.164.tgz",
"integrity": "sha512-IjJZKRhFbWSOX1w0sdIXgp4CMRguu6UYcTckyFF/Gjtemsu/25eZ+RXwFlV+UWcIueHyQA1UnRJxocTpH5NdGA==", "integrity": "sha512-VLlalqUeduN4+fayVtRZvGP2Hl1WrRxlwzh2XVVMJym3IFrQUS29BFQ1GP/BxOJXJI1OFCrJ5BnFEsAe8NHtOg==",
"dev": true "dev": true
}, },
"elliptic": { "elliptic": {
@ -10755,9 +10763,9 @@
} }
}, },
"node-releases": { "node-releases": {
"version": "1.1.1", "version": "1.1.23",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.1.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.23.tgz",
"integrity": "sha512-2UXrBr6gvaebo5TNF84C66qyJJ6r0kxBObgZIDX3D3/mt1ADKiHux3NJPWisq0wxvJJdkjECH+9IIKYViKj71Q==", "integrity": "sha512-uq1iL79YjfYC0WXoHbC/z28q/9pOl8kSHaXdWmAAc8No+bDwqkZbzIJz55g/MUsPgSGm9LZ7QSUbzTcH5tz47w==",
"dev": true, "dev": true,
"requires": { "requires": {
"semver": "^5.3.0" "semver": "^5.3.0"

16
src/Squidex/package.json

@ -47,21 +47,23 @@
"zone.js": "0.9.1" "zone.js": "0.9.1"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "7.2.14",
"@angular/compiler": "7.2.14", "@angular/compiler": "7.2.14",
"@angular/compiler-cli": "7.2.14",
"@ngtools/webpack": "7.3.8", "@ngtools/webpack": "7.3.8",
"@types/core-js": "2.5.0", "@types/core-js": "2.5.0",
"@types/jasmine": "3.3.12", "@types/jasmine": "3.3.12",
"@types/marked": "0.6.5", "@types/marked": "0.6.5",
"@types/mousetrap": "1.6", "@types/mousetrap": "1.6",
"@types/node": "12.0.0", "@types/node": "12.0.0",
"@types/react-dom": "16.8.4",
"@types/react": "16.8.16", "@types/react": "16.8.16",
"@types/react-dom": "16.8.4",
"@types/sortablejs": "1.7.2", "@types/sortablejs": "1.7.2",
"angular-router-loader": "0.8.5", "angular-router-loader": "0.8.5",
"angular2-template-loader": "0.6.2", "angular2-template-loader": "0.6.2",
"awesome-typescript-loader": "5.2.1", "awesome-typescript-loader": "5.2.1",
"babel-core": "6.26.3", "babel-core": "6.26.3",
"browserslist": "^4.6.3",
"caniuse-lite": "^1.0.30000975",
"circular-dependency-plugin": "5.0.2", "circular-dependency-plugin": "5.0.2",
"codelyzer": "5.0.1", "codelyzer": "5.0.1",
"cpx": "1.5.0", "cpx": "1.5.0",
@ -72,16 +74,16 @@
"ignore-loader": "0.1.2", "ignore-loader": "0.1.2",
"istanbul-instrumenter-loader": "3.0.1", "istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "3.4.0", "jasmine-core": "3.4.0",
"karma": "4.1.0",
"karma-chrome-launcher": "2.2.0", "karma-chrome-launcher": "2.2.0",
"karma-cli": "2.0.0", "karma-cli": "2.0.0",
"karma-coverage-istanbul-reporter": "2.0.5", "karma-coverage-istanbul-reporter": "2.0.5",
"karma-htmlfile-reporter": "0.3.8", "karma-htmlfile-reporter": "0.3.8",
"karma-jasmine-html-reporter": "1.4.2",
"karma-jasmine": "2.0.1", "karma-jasmine": "2.0.1",
"karma-jasmine-html-reporter": "1.4.2",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-sourcemap-loader": "0.3.7", "karma-sourcemap-loader": "0.3.7",
"karma-webpack": "3.0.5", "karma-webpack": "3.0.5",
"karma": "4.1.0",
"mini-css-extract-plugin": "0.6.0", "mini-css-extract-plugin": "0.6.0",
"node-sass": "4.12.0", "node-sass": "4.12.0",
"optimize-css-assets-webpack-plugin": "5.0.1", "optimize-css-assets-webpack-plugin": "5.0.1",
@ -93,15 +95,15 @@
"style-loader": "0.23.1", "style-loader": "0.23.1",
"ts-loader": "5.4.5", "ts-loader": "5.4.5",
"tsconfig-paths-webpack-plugin": "3.2.0", "tsconfig-paths-webpack-plugin": "3.2.0",
"tslint-webpack-plugin": "2.0.4",
"tslint": "5.16.0", "tslint": "5.16.0",
"tslint-webpack-plugin": "2.0.4",
"typemoq": "2.1.0", "typemoq": "2.1.0",
"typescript": "3.2.4", "typescript": "3.2.4",
"uglifyjs-webpack-plugin": "2.1.2", "uglifyjs-webpack-plugin": "2.1.2",
"underscore": "1.9.1", "underscore": "1.9.1",
"webpack": "4.30.0",
"webpack-cli": "3.3.1", "webpack-cli": "3.3.1",
"webpack-dev-server": "3.3.1", "webpack-dev-server": "3.3.1",
"webpack-merge": "4.2.1", "webpack-merge": "4.2.1"
"webpack": "4.30.0"
} }
} }

84
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<string>.Ignored)) A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored))
.Returns(new List<IAssetEntity>()); .Returns(new List<IAssetEntity>());
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored)) A.CallTo(() => tagService.DenormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>()); .Returns(new Dictionary<string, string>
{
["1"] = "foundTag1",
["2"] = "foundTag2"
});
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset); .Returns(asset);
sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }); sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }, tagService);
} }
[Fact] [Fact]
public async Task Create_should_create_domain_object() 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); var context = CreateContextForCommand(command);
SetupTags(command); SetupTags(command);
@ -80,6 +84,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Contains("tag1", command.Tags); Assert.Contains("tag1", command.Tags);
Assert.Contains("tag2", command.Tags); Assert.Contains("tag2", command.Tags);
Assert.Equal(new HashSet<string> { "tag1", "tag2" }, result.Tags);
AssertAssetHasBeenUploaded(0, context.ContextId); AssertAssetHasBeenUploaded(0, context.ContextId);
AssertAssetImageChecked(); AssertAssetImageChecked();
} }
@ -87,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Create_should_calculate_hash() 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); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -100,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() 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); var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _); SetupSameHashAsset(file.FileName, file.FileSize, out _);
@ -108,13 +114,15 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.HandleAsync(context); await sut.HandleAsync(context);
Assert.True(context.Result<AssetCreatedResult>().IsDuplicate); var result = context.Result<AssetCreatedResult>();
Assert.True(result.IsDuplicate);
} }
[Fact] [Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found() 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); var context = CreateContextForCommand(command);
SetupSameHashAsset("other-name", file.FileSize, out _); SetupSameHashAsset("other-name", file.FileSize, out _);
@ -122,13 +130,31 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.HandleAsync(context); await sut.HandleAsync(context);
Assert.False(context.Result<AssetCreatedResult>().IsDuplicate); var result = context.Result<AssetCreatedResult>();
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<AssetCreatedResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
} }
[Fact] [Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found() 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); var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, 12345, out _); SetupSameHashAsset(file.FileName, 12345, out _);
@ -142,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Update_should_update_domain_object() 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); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -158,7 +184,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Update_should_calculate_hash() 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); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -170,6 +196,40 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.True(command.FileHash.Length > 10); 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<AssetResult>();
Assert.Equal(new HashSet<string> { "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<AssetResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
}
private Task ExecuteCreateAsync() private Task ExecuteCreateAsync()
{ {
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file })); return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file }));

48
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -212,8 +212,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", assetId.ToString()); }".Replace("<ID>", assetId.ToString());
A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId)) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId)))
.Returns(asset); .Returns(ResultList.Create(1, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -544,8 +544,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(content); .Returns(ResultList.Create(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -635,8 +635,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(content); .Returns(ResultList.Create(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -730,12 +730,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A<Q>.Ignored)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A<Q>.Ignored))
.Returns(ResultList.Create(0, contentRef)); .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 result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
var expected = new var expected = new
@ -788,8 +788,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(content); .Returns(ResultList.Create(1, content));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.Ignored)) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, assetRef)); .Returns(ResultList.Create(0, assetRef));
@ -844,10 +844,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", assetId2.ToString()); }".Replace("<ID>", assetId2.ToString());
A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId1)) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId1)))
.Returns(asset1); .Returns(ResultList.Create(0, asset1));
A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId2))
.Returns(asset2); 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 }); 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("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(content); .Returns(ResultList.Create(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -940,8 +941,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(content); .Returns(ResultList.Create(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -986,8 +987,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(content); .Returns(ResultList.Create(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -1005,6 +1006,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
AssertResult(expected, result); AssertResult(expected, result);
} }
private static Q MatchId(Guid contentId)
{
return A<Q>.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId);
}
private QueryContext MatchsAssetContext() private QueryContext MatchsAssetContext()
{ {
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user); return A<QueryContext>.That.Matches(x => x.App == app && x.User == user);

42
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -9,6 +9,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using FakeItEasy; using FakeItEasy;
using GraphQL;
using GraphQL.DataLoader;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -24,6 +26,7 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
using Xunit; using Xunit;
#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter #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 public class GraphQLTestBase
{ {
protected readonly Schema schemaDef;
protected readonly Guid schemaId = Guid.NewGuid(); protected readonly Guid schemaId = Guid.NewGuid();
protected readonly Guid appId = Guid.NewGuid(); protected readonly Guid appId = Guid.NewGuid();
protected readonly string appName = "my-app"; protected readonly string appName = "my-app";
@ -41,8 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
protected readonly ISchemaEntity schema = A.Fake<ISchemaEntity>(); protected readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None);
protected readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); protected readonly IDependencyResolver dependencyResolver;
protected readonly IAppProvider appProvider = A.Fake<IAppProvider>();
protected readonly IAppEntity app = A.Dummy<IAppEntity>(); protected readonly IAppEntity app = A.Dummy<IAppEntity>();
protected readonly QueryContext context; protected readonly QueryContext context;
protected readonly ClaimsPrincipal user = new ClaimsPrincipal(); protected readonly ClaimsPrincipal user = new ClaimsPrincipal();
@ -50,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public GraphQLTestBase() public GraphQLTestBase()
{ {
schemaDef = var schemaDef =
new Schema("my-schema") new Schema("my-schema")
.AddJson(1, "my-json", Partitioning.Invariant, .AddJson(1, "my-json", Partitioning.Invariant,
new JsonFieldProperties()) new JsonFieldProperties())
@ -92,11 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => schema.Id).Returns(schemaId); A.CallTo(() => schema.Id).Returns(schemaId);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
var allSchemas = new List<ISchemaEntity> { schema }; sut = CreateSut();
A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas);
sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator());
} }
protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null) 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); return serializer.Serialize(result);
} }
private CachingGraphQLService CreateSut()
{
var appProvider = A.Fake<IAppProvider>();
A.CallTo(() => appProvider.GetSchemasAsync(appId))
.Returns(new List<ISchemaEntity> { schema });
var dataLoaderContext = new DataLoaderContextAccessor();
var services = new Dictionary<Type, object>
{
[typeof(IAppProvider)] = appProvider,
[typeof(IAssetQueryService)] = assetQuery,
[typeof(IContentQueryService)] = contentQuery,
[typeof(IDataLoaderContextAccessor)] = dataLoaderContext,
[typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(),
[typeof(ISemanticLog)] = A.Fake<ISemanticLog>(),
[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);
}
} }
} }

Loading…
Cancel
Save