Browse Source

Merge branch 'hateaos' into workflow-generalization

pull/369/head
Sebastian 7 years ago
parent
commit
20e5a94173
  1. 6
      .testrunsettings
  2. 12
      Dockerfile
  3. 12
      Dockerfile.build
  4. 64
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  5. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  6. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs
  7. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  8. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  9. 20
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  10. 11
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  11. 8
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  12. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  13. 42
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs
  14. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  15. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  16. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  17. 11
      src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs
  18. 8
      src/Squidex.Infrastructure/CollectionExtensions.cs
  19. 6
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  20. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  21. 10
      src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs
  22. 5
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs
  23. 6
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  24. 27
      src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
  25. 8
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  26. 3
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  27. 1
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  28. 2
      src/Squidex/app/shared/components/schema-category.component.ts
  29. 2
      src/Squidex/app/shared/services/assets.service.ts
  30. 67
      src/Squidex/app/shared/services/rules.service.spec.ts
  31. 12
      src/Squidex/app/shared/services/rules.service.ts
  32. 2
      src/Squidex/app/shared/services/schemas.service.ts
  33. 8
      src/Squidex/app/shared/state/rule-events.state.spec.ts
  34. 17
      src/Squidex/app/shared/state/schemas.state.spec.ts
  35. 2
      src/Squidex/app/shared/state/schemas.state.ts
  36. 38
      src/Squidex/package-lock.json
  37. 16
      src/Squidex/package.json
  38. 84
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  39. 3
      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>

12
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 \

12
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 \

64
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<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
IAssetQueryService assetQuery,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
IEnumerable<ITagGenerator<CreateAsset>> 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<Task> next)
@ -56,9 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.Tags = new HashSet<string>();
}
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<string>(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<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)
{
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);
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.
// ==========================================================================
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<string> tags)
: base(asset, tags)
{
Asset = asset;
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.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<Guid> AppId { get; set; }
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset()
{
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.
// ==========================================================================
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; }
}
}

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

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

@ -12,6 +12,7 @@ using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
@ -20,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly ISemanticLog log;
private readonly IAssetQueryService assetQuery;
private readonly IAppProvider appProvider;
@ -28,18 +30,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IAppProvider appProvider,
IAssetQueryService assetQuery,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
IGraphQLUrlGenerator urlGenerator,
ISemanticLog log)
: base(cache)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
Guard.NotNull(log, nameof(log));
this.appProvider = appProvider;
this.assetQuery = assetQuery;
this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator;
this.log = log;
}
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries)
@ -70,14 +75,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
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))
{
return (false, new { data = new object() });
}
var result = await model.ExecuteAsync(ctx, query);
var result = await model.ExecuteAsync(ctx, query, log);
if (result.Errors?.Any() == true)
{

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

@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using GraphQLSchema = GraphQL.Types.Schema;
#pragma warning disable IDE0003
@ -135,9 +136,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()
@ -171,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return contentTypes.GetOrAdd(schema, s => new ContentGraphType());
}
public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query)
public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query, ISemanticLog log)
{
Guard.NotNull(context, nameof(context));
@ -179,6 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var result = await new DocumentExecuter().ExecuteAsync(options =>
{
options.FieldMiddleware.Use(LoggingMiddleware.Create(log));
options.OperationName = query.OperationName;
options.UserContext = context;
options.Schema = graphQLSchema;

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 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())
{
var (resolvedType, valueResolver) = model.GetGraphType(schema, field);
var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName);
if (valueResolver != null)
{

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 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<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 IGraphModel model;
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.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);
}

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

@ -1,4 +1,5 @@
// ==========================================================================

// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -22,17 +23,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);
}
}
}

8
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -13,6 +13,14 @@ namespace Squidex.Infrastructure
{
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)
{
var random = new Random();

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 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);
}
@ -267,8 +267,8 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAssetEntity>();
var response = AssetDto.FromAsset(result, this, app);
var result = context.Result<AssetResult>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags);
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")]
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() });
if (tags != null)
{
response.Tags = tags;
}
if (isDuplicate)
{
response.Metadata = new AssetMetadata { IsDuplicate = "true" };

10
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; }
/// <summary>
/// The user who schedule the content.
/// The target date and time when the content should be scheduled.
/// </summary>
public RefToken ScheduledBy { get; set; }
public Instant DueTime { get; set; }
/// <summary>
/// The target date and time when the content should be scheduled.
/// The user who schedule the content.
/// </summary>
public Instant DueTime { get; set; }
[Required]
public RefToken ScheduledBy { get; set; }
}
}

5
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));
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values));
if (NextAttempt.HasValue)
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values));
}
return this;
}

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

@ -513,9 +513,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiCosts(1)]
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)

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

@ -53,22 +53,22 @@
{
if (Model.IsLogin)
{
if (Model.IsFailed)
{
<div class="form-alert form-alert-error">Email or password not correct.</div>
}
if (Model.IsFailed)
{
<div class="form-alert form-alert-error">Email or password not correct.</div>
}
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model.ReturnUrl" method="post">
<div class="form-group">
<input type="email" class="form-control" name="email" id="email" placeholder="Enter Email" />
</div>
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model.ReturnUrl" method="post">
<div class="form-group">
<input type="email" class="form-control" name="email" id="email" placeholder="Enter Email" />
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" id="password" placeholder="Enter Password" />
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" id="password" placeholder="Enter Password" />
</div>
<button type="submit" class="btn btn-block btn-primary">@type</button>
</form>
<button type="submit" class="btn btn-block btn-primary">@type</button>
</form>
}
else
{
@ -95,7 +95,6 @@ else
var redirectButtons = document.getElementsByClassName("redirect-button");
if (redirectButtons.length === 1) {
debugger;
redirectButtons[0].click();
}
</script>

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

@ -22,13 +22,13 @@
</td>
<td class="text-center">
<input type="text" class="form-control code" placeholder="Optional condition as javascript expression"
[disabled]="!isEditable"
[disabled]="triggerForm.disabled"
[ngModelOptions]="{ updateOn: 'blur' }"
[ngModel]="triggerSchema.condition"
(ngModelChange)="updateCondition(triggerSchema.schema, $event)" />
</td>
<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>
</button>
</td>
@ -38,12 +38,12 @@
<div class="section" *ngIf="schemasToAdd.length > 0">
<form class="form-inline" (ngSubmit)="addSchema()">
<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>
</select>
</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>
</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()
public schemas: ImmutableArray<SchemaDto>;
@Input()
public isEditable: boolean;
@Input()
public trigger: any;

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

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

2
src/Squidex/app/shared/components/schema-category.component.ts

@ -96,7 +96,7 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
if (query) {
isOpen = true;
} else {
isOpen = this.localStore.get(`schema-category.${this.name}`) !== 'false';
isOpen = !this.localStore.getBoolean(this.configKey());
}
this.next(s => ({ ...s, isOpen, schemasFiltered, schemasForCategory }));

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

@ -242,7 +242,7 @@ export class AssetsService {
tap(() => {
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>> {

67
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}` }

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> {
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<RuleEventDto> {
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,

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

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 {
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<DialogService>;

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

@ -13,6 +13,7 @@ import { SchemasState } from './schemas.state';
import {
DialogService,
FieldDto,
SchemaDetailsDto,
SchemasService,
UpdateSchemaCategoryDto,
@ -329,28 +330,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 = <SchemaDetailsDto>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 = <SchemaDetailsDto>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', () => {

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

@ -322,7 +322,7 @@ export class SchemasState extends State<Snapshot> {
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)!;
}

38
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"

16
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"
}
}

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))
.Returns(new List<IAssetEntity>());
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>());
A.CallTo(() => tagService.DenormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>
{
["1"] = "foundTag1",
["2"] = "foundTag2"
});
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(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<string> { "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<AssetCreatedResult>().IsDuplicate);
var result = context.Result<AssetCreatedResult>();
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<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]
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<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()
{
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file }));

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

@ -24,6 +24,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
@ -96,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas);
sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator());
sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator(), A.Fake<ISemanticLog>());
}
protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null)

Loading…
Cancel
Save