Browse Source

Temp

pull/355/head
Sebastian Stehle 7 years ago
parent
commit
88d3b43e15
  1. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs
  2. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  3. 42
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  4. 2
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  5. 75
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  6. 21
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  7. 40
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  8. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  9. 5
      src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  11. 2
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  12. 6
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  13. 5
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  14. 2
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  15. 2
      src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
  16. 2
      src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  17. 2
      src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs
  18. 2
      src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs
  19. 96
      src/Squidex.Infrastructure/Assets/HasherStream.cs
  20. 10
      src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs
  21. 2
      src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs
  22. 8
      src/Squidex.Infrastructure/RandomHash.cs
  23. 42
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  24. 14
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs
  25. 6
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  26. 6
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs
  27. 2
      src/Squidex/Squidex.csproj
  28. 1
      src/Squidex/app/features/administration/declarations.ts
  29. 2
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  30. 14
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  31. 44
      src/Squidex/app/features/administration/services/users.service.ts
  32. 4
      src/Squidex/app/features/administration/state/event-consumers.state.spec.ts
  33. 60
      src/Squidex/app/features/administration/state/users.forms.ts
  34. 11
      src/Squidex/app/features/administration/state/users.state.spec.ts
  35. 60
      src/Squidex/app/features/administration/state/users.state.ts
  36. 23
      src/Squidex/app/framework/state.ts
  37. 12
      src/Squidex/app/shared/components/assets-list.component.ts
  38. 6
      src/Squidex/app/shared/services/app-clients.service.spec.ts
  39. 32
      src/Squidex/app/shared/services/app-clients.service.ts
  40. 3
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  41. 31
      src/Squidex/app/shared/services/app-contributors.service.ts
  42. 6
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  43. 22
      src/Squidex/app/shared/services/app-languages.service.ts
  44. 5
      src/Squidex/app/shared/services/app-patterns.service.spec.ts
  45. 18
      src/Squidex/app/shared/services/app-patterns.service.ts
  46. 6
      src/Squidex/app/shared/services/app-roles.service.spec.ts
  47. 22
      src/Squidex/app/shared/services/app-roles.service.ts
  48. 3
      src/Squidex/app/shared/services/apps.service.spec.ts
  49. 15
      src/Squidex/app/shared/services/apps.service.ts
  50. 32
      src/Squidex/app/shared/services/assets.service.spec.ts
  51. 67
      src/Squidex/app/shared/services/assets.service.ts
  52. 7
      src/Squidex/app/shared/services/backups.service.spec.ts
  53. 14
      src/Squidex/app/shared/services/backups.service.ts
  54. 9
      src/Squidex/app/shared/services/comments.service.spec.ts
  55. 15
      src/Squidex/app/shared/services/comments.service.ts
  56. 21
      src/Squidex/app/shared/services/contents.service.ts
  57. 5
      src/Squidex/app/shared/services/plans.service.spec.ts
  58. 20
      src/Squidex/app/shared/services/plans.service.ts
  59. 32
      src/Squidex/app/shared/services/rules.service.spec.ts
  60. 40
      src/Squidex/app/shared/services/rules.service.ts
  61. 11
      src/Squidex/app/shared/services/schemas.service.spec.ts
  62. 47
      src/Squidex/app/shared/services/schemas.service.ts
  63. 18
      src/Squidex/app/shared/services/schemas.types.ts
  64. 7
      src/Squidex/app/shared/services/translations.service.spec.ts
  65. 11
      src/Squidex/app/shared/services/translations.service.ts
  66. 6
      src/Squidex/app/shared/services/ui.service.ts
  67. 11
      src/Squidex/app/shared/state/apps.state.spec.ts
  68. 8
      src/Squidex/app/shared/state/assets.state.spec.ts
  69. 12
      src/Squidex/app/shared/state/clients.state.spec.ts
  70. 19
      src/Squidex/app/shared/state/comments.state.spec.ts
  71. 11
      src/Squidex/app/shared/state/comments.state.ts
  72. 3
      src/Squidex/app/shared/state/contributors.state.spec.ts
  73. 3
      src/Squidex/app/shared/state/languages.state.spec.ts
  74. 22
      src/Squidex/app/shared/state/languages.state.ts
  75. 11
      src/Squidex/app/shared/state/patterns.state.spec.ts
  76. 2
      src/Squidex/app/shared/state/patterns.state.ts
  77. 4
      src/Squidex/app/shared/state/plans.state.spec.ts
  78. 9
      src/Squidex/app/shared/state/plans.state.ts
  79. 6
      src/Squidex/app/shared/state/roles.state.spec.ts
  80. 3
      src/Squidex/app/shared/state/rules.state.spec.ts
  81. 7
      src/Squidex/app/shared/state/rules.state.ts
  82. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  83. 112
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  84. 15
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  85. 15
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  86. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs
  87. 47
      tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs
  88. 10
      tests/Squidex.Infrastructure.Tests/DispatchingTests.cs

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs

@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
string FileName { get; }
string FileHash { get; }
string Slug { get; }
}
}

4
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -38,6 +38,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement]
public string FileName { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public string FileHash { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public string Slug { get; set; }

42
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -46,7 +46,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.Ascending(x => x.Tags)
.Descending(x => x.LastModified)),
new CreateIndexModel<MongoAssetEntity>(
Index.Ascending(x => x.Slug))
Index
.Ascending(x => x.AppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.FileHash)),
new CreateIndexModel<MongoAssetEntity>(
Index
.Ascending(x => x.AppId)
.Ascending(x => x.Slug))
},
ct);
}
@ -102,19 +109,41 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
public async Task<IAssetEntity> FindAssetAsync(string slug)
public async Task<IAssetEntity> FindAssetBySlugAsync(Guid appId, string slug)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetEntity =
await Collection.Find(x => x.Slug == slug)
await Collection.Find(x => x.IndexedAppId == appId && x.Slug == slug)
.FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true)
{
return null;
}
return assetEntity;
}
}
public async Task<IAssetEntity> FindAssetAsync(Guid id)
public async Task<IAssetEntity> FindAssetByHashAsync(Guid appId, string hash)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetEntity =
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash)
.FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true)
{
return null;
}
return assetEntity;
}
}
public async Task<IAssetEntity> FindAssetAsync(Guid id, bool allowDeleted = false)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
@ -122,6 +151,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.Id == id)
.FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true && !allowDeleted)
{
return null;
}
return assetEntity;
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -14,6 +14,8 @@ using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Apps.State
{
[CollectionName("Apps")]

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

@ -7,9 +7,11 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
@ -20,21 +22,25 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain>
{
private readonly IAssetStore assetStore;
private readonly AssetQueryService assetQueryService;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
AssetQueryService assetQueryService,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
: base(grainFactory)
{
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQueryService, nameof(assetQueryService));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators));
this.assetStore = assetStore;
this.assetQueryService = assetQueryService;
this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators;
@ -53,21 +59,45 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead());
foreach (var tagGenerator in tagGenerators)
{
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
createAsset.FileHash = await UploadAsync(context, createAsset.File);
var originalTags = new HashSet<string>(createAsset.Tags);
await assetStore.UploadAsync(context.ContextId.ToString(), createAsset.File.OpenRead());
try
{
var result = (AssetSavedResult)await ExecuteCommandAsync(createAsset);
context.Complete(new AssetCreatedResult(createAsset.AssetId, originalTags, result.Version));
var existing = await assetQueryService.FindAssetByHashAsync(createAsset.AppId.Id, createAsset.FileHash);
AssetCreatedResult result;
if (IsDuplicate(createAsset, existing))
{
result = new AssetCreatedResult(
existing.Id,
existing.Tags,
existing.Version,
existing.FileVersion,
existing.FileHash,
true);
}
else
{
foreach (var tagGenerator in tagGenerators)
{
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
var commandResult = (AssetSavedResult)await ExecuteCommandAsync(createAsset);
result = new AssetCreatedResult(
createAsset.AssetId,
createAsset.Tags,
commandResult.Version,
commandResult.FileVersion,
commandResult.FileHash,
false);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null);
}
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null);
context.Complete(result);
}
finally
{
@ -81,10 +111,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead());
await assetStore.UploadAsync(context.ContextId.ToString(), updateAsset.File.OpenRead());
updateAsset.FileHash = await UploadAsync(context, updateAsset.File);
try
{
var result = await ExecuteCommandAsync(updateAsset) as AssetSavedResult;
var result = (AssetSavedResult)await ExecuteCommandAsync(updateAsset);
context.Complete(result);
@ -103,5 +133,24 @@ namespace Squidex.Domain.Apps.Entities.Assets
break;
}
}
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)
{
string hash;
using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256))
{
await assetStore.UploadAsync(context.ContextId.ToString(), hashStream);
hash = hashStream.GetHashStringAndReset();
}
return hash;
}
}
}

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

@ -11,18 +11,25 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetCreatedResult : EntitySavedResult
public sealed class AssetCreatedResult : EntityCreatedResult<Guid>
{
public Guid Id { get; }
public HashSet<string> Tags { get; }
public AssetCreatedResult(Guid id, HashSet<string> tags, long version)
: base(version)
{
Id = id;
public long FileVersion { get; }
public string FileHash { get; }
public bool IsDuplicate { get; }
public AssetCreatedResult(Guid id, HashSet<string> tags, long version, long fileVersion, string fileHash, bool isDuplicate)
: base(id, version)
{
Tags = tags;
FileVersion = fileVersion;
FileHash = fileHash;
IsDuplicate = isDuplicate;
}
}
}

40
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -47,11 +47,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
GuardAsset.CanCreate(c);
c.Tags = await NormalizeTagsAsync(c.AppId.Id, c.Tags);
var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags);
Create(c);
Create(c, tagIds);
return new AssetSavedResult(Version, Snapshot.FileVersion);
return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash);
});
case UpdateAsset updateRule:
return UpdateAsync(updateRule, c =>
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
Update(c);
return new AssetSavedResult(Version, Snapshot.FileVersion);
return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash);
});
case DeleteAsset deleteAsset:
return UpdateAsync(deleteAsset, async c =>
@ -76,12 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
if (c.Tags != null)
{
c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
}
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Annotate(c);
Annotate(c, tagIds);
});
default:
throw new NotSupportedException();
@ -90,32 +87,37 @@ namespace Squidex.Domain.Apps.Entities.Assets
private async Task<HashSet<string>> NormalizeTagsAsync(Guid appId, HashSet<string> tags)
{
if (tags == null)
{
return null;
}
var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags);
return new HashSet<string>(normalized.Values);
}
public void Create(CreateAsset command)
public void Create(CreateAsset command, HashSet<string> tagIds)
{
var @event = SimpleMapper.Map(command, new AssetCreated
{
IsImage = command.ImageInfo != null,
FileName = command.File.FileName,
FileSize = command.File.FileSize,
FileVersion = 0,
MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null,
Slug = command.File.FileName.ToAssetSlug()
});
@event.Tags = tagIds;
RaiseEvent(@event);
}
public void Update(UpdateAsset command)
{
VerifyNotDeleted();
var @event = SimpleMapper.Map(command, new AssetUpdated
{
FileVersion = Snapshot.FileVersion + 1,
@ -129,14 +131,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
RaiseEvent(@event);
}
public void Delete(DeleteAsset command)
public void Annotate(AnnotateAsset command, HashSet<string> tagIds)
{
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize }));
var @event = SimpleMapper.Map(command, new AssetAnnotated());
@event.Tags = tagIds;
RaiseEvent(@event);
}
public void Annotate(AnnotateAsset command)
public void Delete(DeleteAsset command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetAnnotated()));
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize }));
}
private void RaiseEvent(AppEvent @event)

25
src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs

@ -21,7 +21,7 @@ using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetQueryService : IAssetQueryService
public class AssetQueryService : IAssetQueryService
{
private readonly ITagService tagService;
private readonly IAssetRepository assetRepository;
@ -38,21 +38,38 @@ namespace Squidex.Domain.Apps.Entities.Assets
this.tagService = tagService;
}
public async Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id)
public virtual Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id)
{
Guard.NotNull(context, nameof(context));
return FindAssetAsync(context.App.Id, id);
}
public virtual async Task<IAssetEntity> FindAssetAsync(Guid appId, Guid id)
{
var asset = await assetRepository.FindAssetAsync(id);
if (asset != null)
{
await DenormalizeTagsAsync(context.App.Id, Enumerable.Repeat(asset, 1));
await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1));
}
return asset;
}
public virtual async Task<IAssetEntity> FindAssetByHashAsync(Guid appId, string hash)
{
var asset = await assetRepository.FindAssetByHashAsync(appId, hash);
if (asset != null)
{
await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1));
}
return asset;
}
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query)
public virtual async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));

5
src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs

@ -13,10 +13,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public long FileVersion { get; }
public AssetSavedResult(long version, long fileVersion)
public string FileHash { get; }
public AssetSavedResult(long version, long fileVersion, string fileHash)
: base(version)
{
FileVersion = fileVersion;
FileHash = fileHash;
}
}
}

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

@ -22,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset()
{
AssetId = Guid.NewGuid();

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

@ -14,5 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

6
src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -19,9 +19,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
Task<IAssetEntity> FindAssetAsync(string slug);
Task<IAssetEntity> FindAssetAsync(Guid id, bool allowDeleted = false);
Task<IAssetEntity> FindAssetAsync(Guid id);
Task<IAssetEntity> FindAssetBySlugAsync(Guid appId, string slug);
Task<IAssetEntity> FindAssetByHashAsync(Guid appId, string hash);
Task RemoveAsync(Guid appId);
}

5
src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs

@ -16,6 +16,8 @@ using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Assets.State
{
public class AssetState : DomainObjectState<AssetState>, IAssetEntity
@ -26,6 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[DataMember]
public string FileName { get; set; }
[DataMember]
public string FileHash { get; set; }
[DataMember]
public string MimeType { get; set; }

2
src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -15,6 +15,8 @@ using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Contents.State
{
public class ContentState : DomainObjectState<ContentState>, IContentEntity

2
src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs

@ -15,6 +15,8 @@ using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Rules.State
{
[CollectionName("Rules")]

2
src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs

@ -16,6 +16,8 @@ using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Schemas.State
{
[CollectionName("Schemas")]

2
src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Events.Assets
{
public string FileName { get; set; }
public string FileHash { get; set; }
public string MimeType { get; set; }
public string Slug { get; set; }

2
src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Events.Assets
{
public string MimeType { get; set; }
public string FileHash { get; set; }
public long FileSize { get; set; }
public long FileVersion { get; set; }

96
src/Squidex.Infrastructure/Assets/HasherStream.cs

@ -0,0 +1,96 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Security.Cryptography;
namespace Squidex.Infrastructure.Assets
{
public sealed class HasherStream : Stream
{
private readonly Stream inner;
private readonly IncrementalHash hasher;
public override bool CanRead
{
get { return inner.CanRead; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
public override long Length
{
get { return inner.Length; }
}
public override long Position
{
get { return inner.Position; }
set { throw new NotSupportedException(); }
}
public HasherStream(Stream inner, HashAlgorithmName hashAlgorithmName)
{
Guard.NotNull(inner, nameof(inner));
this.inner = inner;
hasher = IncrementalHash.CreateHash(hashAlgorithmName);
}
public override int Read(byte[] buffer, int offset, int count)
{
var read = inner.Read(buffer, offset, count);
if (read > 0)
{
hasher.AppendData(buffer, offset, read);
}
return read;
}
public byte[] GetHashAndReset()
{
return hasher.GetHashAndReset();
}
public string GetHashStringAndReset()
{
return Convert.ToBase64String(GetHashAndReset());
}
public override void Flush()
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
}
}

10
src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Assets
{
public sealed class MemoryAssetStore : IAssetStore
public class MemoryAssetStore : IAssetStore
{
private readonly ConcurrentDictionary<string, MemoryStream> streams = new ConcurrentDictionary<string, MemoryStream>();
private readonly AsyncLock readerLock = new AsyncLock();
@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
public virtual async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName));
Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName));
@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default)
public virtual async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName, nameof(fileName));
@ -62,7 +62,7 @@ namespace Squidex.Infrastructure.Assets
}
}
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default)
public virtual async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName, nameof(fileName));
@ -99,7 +99,7 @@ namespace Squidex.Infrastructure.Assets
}
}
public Task DeleteAsync(string fileName)
public virtual Task DeleteAsync(string fileName)
{
Guard.NotNullOrEmpty(fileName, nameof(fileName));

2
src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs

@ -7,7 +7,7 @@
namespace Squidex.Infrastructure.Commands
{
public sealed class EntityCreatedResult<T> : EntitySavedResult
public class EntityCreatedResult<T> : EntitySavedResult
{
public T IdOrValue { get; }

8
src/Squidex.Infrastructure/RandomHash.cs

@ -19,11 +19,15 @@ namespace Squidex.Infrastructure
}
public static string Sha256Base64(this string value)
{
return Sha256Base64(Encoding.UTF8.GetBytes(value));
}
public static string Sha256Base64(this byte[] bytes)
{
using (var sha = SHA256.Create())
{
var bytesValue = Encoding.UTF8.GetBytes(value);
var bytesHash = sha.ComputeHash(bytesValue);
var bytesHash = sha.ComputeHash(bytes);
var result = Convert.ToBase64String(bytesHash);

42
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -62,7 +62,38 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("assets/{id}/{*more}")]
[ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0.5)]
public async Task<IActionResult> GetAssetContent(string id, string more,
public async Task<IActionResult> GetAssetContent(Guid id, string more,
[FromQuery] long version = EtagVersion.Any,
[FromQuery] int? width = null,
[FromQuery] int? height = null,
[FromQuery] int? quality = null,
[FromQuery] string mode = null)
{
var entity = await assetRepository.FindAssetAsync(id);
return DeliverAsset(entity, version, width, height, quality, mode);
}
/// <summary>
/// Get the asset content.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="idOrSlug">The id or slug of the asset.</param>
/// <param name="more">Optional suffix that can be used to seo-optimize the link to the image Has not effect.</param>
/// <param name="version">The optional version of the asset.</param>
/// <param name="width">The target width of the asset, if it is an image.</param>
/// <param name="height">The target height of the asset, if it is an image.</param>
/// <param name="quality">Optional image quality, it is is an jpeg image.</param>
/// <param name="mode">The resize mode when the width and height is defined.</param>
/// <returns>
/// 200 => Asset found and content or (resized) image returned.
/// 404 => Asset or app not found.
/// </returns>
[HttpGet]
[Route("assets/{app}/{idOrSlug}/{*more}")]
[ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0.5)]
public async Task<IActionResult> GetAssetContent(string app, string idOrSlug, string more,
[FromQuery] long version = EtagVersion.Any,
[FromQuery] int? width = null,
[FromQuery] int? height = null,
@ -71,15 +102,20 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
IAssetEntity entity;
if (Guid.TryParse(id, out var guid))
if (Guid.TryParse(idOrSlug, out var guid))
{
entity = await assetRepository.FindAssetAsync(guid);
}
else
{
entity = await assetRepository.FindAssetAsync(id);
entity = await assetRepository.FindAssetByHashAsync(App.Id, idOrSlug);
}
return DeliverAsset(entity, version, width, height, quality, mode);
}
private IActionResult DeliverAsset(IAssetEntity entity, long version, int? width, int? height, int? quality, string mode)
{
if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0)
{
return NotFound();

14
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs

@ -76,6 +76,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
public int? PixelHeight { get; set; }
/// <summary>
/// Indicates if the asset has been already uploaded.
/// </summary>
public bool IsDuplicate { get; set; }
/// <summary>
/// The version of the asset.
/// </summary>
@ -83,22 +88,21 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
public static AssetCreatedDto FromCommand(CreateAsset command, AssetCreatedResult result)
{
var response = new AssetCreatedDto
return new AssetCreatedDto
{
Id = command.AssetId,
Id = result.IdOrValue,
FileName = command.File.FileName,
FileSize = command.File.FileSize,
FileType = command.File.FileName.FileType(),
FileVersion = result.Version,
FileVersion = result.FileVersion,
MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null,
IsDuplicate = result.IsDuplicate,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
Tags = result.Tags,
Version = result.Version
};
return response;
}
}
}

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

@ -29,6 +29,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required]
public string FileName { get; set; }
/// <summary>
/// The file hash.
/// </summary>
[Required]
public string FileHash { get; set; }
/// <summary>
/// The slug.
/// </summary>

6
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs

@ -19,6 +19,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required]
public string MimeType { get; set; }
/// <summary>
/// The file hash.
/// </summary>
[Required]
public string FileHash { get; set; }
/// <summary>
/// The size of the file in bytes.
/// </summary>

2
src/Squidex/Squidex.csproj

@ -153,6 +153,6 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605</NoWarn>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060</NoWarn>
</PropertyGroup>
</Project>

1
src/Squidex/app/features/administration/declarations.ts

@ -19,4 +19,5 @@ export * from './services/event-consumers.service';
export * from './services/users.service';
export * from './state/event-consumers.state';
export * from './state/users.forms';
export * from './state/users.state';

2
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -21,7 +21,7 @@ import { EventConsumersState } from './../../state/event-consumers.state';
})
export class EventConsumersPageComponent extends ResourceOwner implements OnInit {
public eventConsumerErrorDialog = new DialogModel();
public eventConsumerError = '';
public eventConsumerError?: string;
constructor(
public readonly eventConsumersState: EventConsumersState

14
src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -16,20 +16,16 @@ import {
pretifyError
} from '@app/shared';
export class EventConsumerDto extends Model {
export class EventConsumerDto extends Model<EventConsumerDto> {
constructor(
public readonly name: string,
public readonly isStopped: boolean,
public readonly isResetting: boolean,
public readonly error: string,
public readonly position: string
public readonly isStopped?: boolean,
public readonly isResetting?: boolean,
public readonly error?: string,
public readonly position?: string
) {
super();
}
public with(value: Partial<EventConsumerDto>): EventConsumerDto {
return this.clone(value);
}
}
@Injectable()

44
src/Squidex/app/features/administration/services/users.service.ts

@ -13,25 +13,19 @@ import { map } from 'rxjs/operators';
import {
ApiUrlConfig,
Model,
pretifyError
pretifyError,
ResultSet
} from '@app/shared';
export class UsersDto extends Model {
constructor(
public readonly total: number,
public readonly items: UserDto[]
) {
super();
}
}
export class UsersDto extends ResultSet<UserDto> {}
export class UserDto extends Model {
export class UserDto extends Model<UserDto> {
constructor(
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly permissions: string[],
public readonly isLocked: boolean
public readonly permissions: string[] = [],
public readonly isLocked?: boolean
) {
super();
}
@ -41,24 +35,18 @@ export class UserDto extends Model {
}
}
export class CreateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly permissions: string[],
public readonly password: string
) {
}
export interface CreateUserDto {
readonly email: string;
readonly displayName: string;
readonly permissions: string[];
readonly password: string;
}
export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly permissions: string[],
public readonly password?: string
) {
}
export interface UpdateUserDto {
readonly email: string;
readonly displayName: string;
readonly permissions: string[];
readonly password?: string;
}
@Injectable()

4
src/Squidex/app/features/administration/state/event-consumers.state.spec.ts

@ -16,8 +16,8 @@ import { EventConsumersState } from './event-consumers.state';
describe('EventConsumersState', () => {
const oldConsumers = [
new EventConsumerDto('name1', false, false, 'error', '1'),
new EventConsumerDto('name2', true, true, 'error', '2')
new EventConsumerDto('name1', false),
new EventConsumerDto('name2', true)
];
let dialogs: IMock<DialogService>;

60
src/Squidex/app/features/administration/state/users.forms.ts

@ -0,0 +1,60 @@
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, ValidatorsEx } from '@app/shared';
import { UserDto } from './../services/users.service';
export class UserForm extends Form<FormGroup> {
constructor(
formBuilder: FormBuilder
) {
super(formBuilder.group({
email: ['',
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]
],
displayName: ['',
[
Validators.required,
Validators.maxLength(100)
]
],
password: ['',
[
Validators.nullValidator
]
],
passwordConfirm: ['',
[
ValidatorsEx.match('password', 'Passwords must be the same.')
]
],
permissions: ['']
}));
}
public load(user?: UserDto) {
if (user) {
this.form.controls['password'].setValidators(null);
super.load({ ...user, permissions: user.permissions.join('\n') });
} else {
this.form.controls['password'].setValidators(Validators.required);
super.load(undefined);
}
}
public submit() {
const result = super.submit();
if (result) {
result['permissions'] = result['permissions'].split('\n').filter((x: any) => !!x);
}
return result;
}
}

11
src/Squidex/app/features/administration/state/users.state.spec.ts

@ -13,8 +13,6 @@ import { AuthService, DialogService } from '@app/shared';
import { UsersState } from './users.state';
import {
CreateUserDto,
UpdateUserDto,
UserDto,
UsersDto,
UsersService
@ -168,7 +166,7 @@ describe('UsersState', () => {
});
it('should update user properties when updated', () => {
const request = new UpdateUserDto('new@mail.com', 'New', ['Permission1']);
const request = { email: 'new@mail.com', displayName: 'New', permissions: ['Permission1'] };
usersService.setup(x => x.putUser('id1', request))
.returns(() => of({}));
@ -178,13 +176,14 @@ describe('UsersState', () => {
const user_1 = usersState.snapshot.users.at(0);
expect(user_1.user.email).toEqual('new@mail.com');
expect(user_1.user.displayName).toEqual('New');
expect(user_1.user.email).toEqual(request.email);
expect(user_1.user.displayName).toEqual(request.permissions);
expect(user_1.user.permissions).toEqual(request.permissions);
expect(user_1).toBe(usersState.snapshot.selectedUser!);
});
it('should add user to snapshot when created', () => {
const request = new CreateUserDto(newUser.email, newUser.displayName, newUser.permissions, 'password');
const request = { ...newUser, password: 'password' };
usersService.setup(x => x.postUser(request))
.returns(() => of(newUser));

60
src/Squidex/app/features/administration/state/users.state.ts

@ -6,7 +6,6 @@
*/
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
@ -15,12 +14,10 @@ import '@app/framework/utils/rxjs-extensions';
import {
AuthService,
DialogService,
Form,
ImmutableArray,
notify,
Pager,
State,
ValidatorsEx
State
} from '@app/shared';
import {
@ -30,61 +27,6 @@ import {
UsersService
} from './../services/users.service';
export class UserForm extends Form<FormGroup> {
constructor(
formBuilder: FormBuilder
) {
super(formBuilder.group({
email: ['',
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]
],
displayName: ['',
[
Validators.required,
Validators.maxLength(100)
]
],
password: ['',
[
Validators.nullValidator
]
],
passwordConfirm: ['',
[
ValidatorsEx.match('password', 'Passwords must be the same.')
]
],
permissions: ['']
}));
}
public load(user?: UserDto) {
if (user) {
this.form.controls['password'].setValidators(null);
super.load({ ...user, permissions: user.permissions.join('\n') });
} else {
this.form.controls['password'].setValidators(Validators.required);
super.load(undefined);
}
}
public submit() {
const result = super.submit();
if (result) {
result['permissions'] = result['permissions'].split('\n').filter((x: any) => !!x);
}
return result;
}
}
interface SnapshotUser {
// The user.
user: UserDto;

23
src/Squidex/app/framework/state.ts

@ -97,9 +97,17 @@ export class Form<T extends AbstractControl> {
}
}
export class Model {
protected clone(update: ((v: any) => object) | object, validOnly = false): any {
let values: object;
export function createModel<T>(c: { new(): T; }, values: Partial<T>): T {
return Object.assign(new c(), values);
}
export class Model<T> {
public with(value: Partial<T>, validOnly = false): T {
return this.clone(value, validOnly);
}
protected clone(update: ((v: any) => T) | Partial<T>, validOnly = false): T {
let values: Partial<T>;
if (Types.isFunction(update)) {
values = update(<any>this);
} else {
@ -126,6 +134,15 @@ export class Model {
}
}
export class ResultSet<T> extends Model<ResultSet<T>> {
constructor(
public readonly total: number,
public readonly items: T[]
) {
super();
}
}
export class State<T extends {}> {
private readonly state: BehaviorSubject<Readonly<T>>;
private readonly initialState: Readonly<T>;

12
src/Squidex/app/shared/components/assets-list.component.ts

@ -11,6 +11,7 @@ import { onErrorResumeNext } from 'rxjs/operators';
import {
AssetDto,
AssetsState,
DialogService,
ImmutableArray
} from '@app/shared/internal';
@ -38,10 +39,19 @@ export class AssetsListComponent {
@Output()
public select = new EventEmitter<AssetDto>();
constructor(
private readonly dialogs: DialogService
) {
}
public add(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file);
this.state.add(asset);
if (asset.isDuplicate) {
this.dialogs.notifyError('The same asset has already been uploaded.');
} else {
this.state.add(asset);
}
}
public search() {

6
src/Squidex/app/shared/services/app-clients.service.spec.ts

@ -15,8 +15,6 @@ import {
AppClientDto,
AppClientsDto,
AppClientsService,
CreateAppClientDto,
UpdateAppClientDto,
Version
} from './../';
@ -83,7 +81,7 @@ describe('AppClientsService', () => {
it('should make post request to create client',
inject([AppClientsService, HttpTestingController], (appClientsService: AppClientsService, httpMock: HttpTestingController) => {
const dto = new CreateAppClientDto('client1');
const dto = { id: 'client1' };
let client: AppClientDto;
@ -104,7 +102,7 @@ describe('AppClientsService', () => {
it('should make put request to rename client',
inject([AppClientsService, HttpTestingController], (appClientsService: AppClientsService, httpMock: HttpTestingController) => {
const dto = new UpdateAppClientDto('Client 1 New');
const dto = { name: 'New Name' };
appClientsService.putClient('my-app', 'client1', dto, version).subscribe();

32
src/Squidex/app/shared/services/app-clients.service.ts

@ -20,7 +20,7 @@ import {
Versioned
} from '@app/framework';
export class AppClientsDto extends Model {
export class AppClientsDto extends Model<AppClientsDto> {
constructor(
public readonly clients: AppClientDto[],
public readonly version: Version
@ -29,42 +29,32 @@ export class AppClientsDto extends Model {
}
}
export class AppClientDto extends Model {
export class AppClientDto extends Model<AppClientDto> {
constructor(
public readonly id: string,
public readonly name: string,
public readonly secret: string,
public readonly role: string
public readonly role = 'Developer'
) {
super();
}
public with(value: Partial<AppClientDto>): AppClientDto {
return this.clone(value);
}
}
export class CreateAppClientDto {
export class AccessTokenDto {
constructor(
public readonly id: string
public readonly accessToken: string,
public readonly tokenType: string
) {
}
}
export class UpdateAppClientDto {
constructor(
public readonly name?: string,
public readonly role?: string
) {
}
export interface CreateAppClientDto {
readonly id: string;
}
export class AccessTokenDto {
constructor(
public readonly accessToken: string,
public readonly tokenType: string
) {
}
export interface UpdateAppClientDto {
readonly name?: string;
readonly role?: string;
}
@Injectable()

3
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -14,7 +14,6 @@ import {
AppContributorDto,
AppContributorsDto,
AppContributorsService,
AssignContributorDto,
ContributorAssignedDto,
Version
} from './../';
@ -81,7 +80,7 @@ describe('AppContributorsService', () => {
it('should make post request to assign contributor',
inject([AppContributorsService, HttpTestingController], (appContributorsService: AppContributorsService, httpMock: HttpTestingController) => {
const dto = new AssignContributorDto('123', 'Owner');
const dto = { contributorId: '123', role: 'Owner' };
let contributorAssignedDto: ContributorAssignedDto;

31
src/Squidex/app/shared/services/app-contributors.service.ts

@ -20,7 +20,7 @@ import {
Versioned
} from '@app/framework';
export class AppContributorsDto extends Model {
export class AppContributorsDto extends Model<AppContributorsDto> {
constructor(
public readonly contributors: AppContributorDto[],
public readonly maxContributors: number,
@ -30,31 +30,24 @@ export class AppContributorsDto extends Model {
}
}
export class AssignContributorDto extends Model {
export class AppContributorDto extends Model<AssignContributorDto> {
constructor(
public readonly contributorId: string,
public readonly role: string,
public readonly invite = false
public readonly role: string
) {
super();
}
}
export class AppContributorDto extends Model {
constructor(
public readonly contributorId: string,
public readonly role: string
) {
super();
}
export interface ContributorAssignedDto {
readonly contributorId: string;
readonly isCreated?: boolean;
}
export class ContributorAssignedDto {
constructor(
public readonly contributorId: string,
public readonly isCreated: boolean
) {
}
export interface AssignContributorDto {
readonly contributorId: string;
readonly role: string;
readonly invite?: boolean;
}
@Injectable()
@ -93,9 +86,7 @@ export class AppContributorsService {
map(response => {
const body: any = response.payload.body;
const result = new ContributorAssignedDto(body.contributorId, body.isCreated);
return new Versioned(response.version, result);
return new Versioned(response.version, body);
}),
tap(() => {
this.analytics.trackEvent('Contributor', 'Configured', appName);

6
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -9,13 +9,11 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/
import { inject, TestBed } from '@angular/core/testing';
import {
AddAppLanguageDto,
AnalyticsService,
ApiUrlConfig,
AppLanguageDto,
AppLanguagesDto,
AppLanguagesService,
UpdateAppLanguageDto,
Version
} from './../';
@ -83,7 +81,7 @@ describe('AppLanguagesService', () => {
it('should make post request to add language',
inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => {
const dto = new AddAppLanguageDto('de');
const dto = { language: 'de' };
let language: AppLanguageDto;
@ -104,7 +102,7 @@ describe('AppLanguagesService', () => {
it('should make put request to make master language',
inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => {
const dto = new UpdateAppLanguageDto(true, true, []);
const dto = { isMaster: true };
appLanguagesService.putLanguage('my-app', 'de', dto, version).subscribe();

22
src/Squidex/app/shared/services/app-languages.service.ts

@ -20,7 +20,7 @@ import {
Versioned
} from '@app/framework';
export class AppLanguagesDto extends Model {
export class AppLanguagesDto extends Model<AppLanguagesDto> {
constructor(
public readonly languages: AppLanguageDto[],
public readonly version: Version
@ -29,7 +29,7 @@ export class AppLanguagesDto extends Model {
}
}
export class AppLanguageDto extends Model {
export class AppLanguageDto extends Model<AppLanguageDto> {
constructor(
public readonly iso2Code: string,
public readonly englishName: string,
@ -41,20 +41,14 @@ export class AppLanguageDto extends Model {
}
}
export class AddAppLanguageDto {
constructor(
public readonly language: string
) {
}
export interface AddAppLanguageDto {
readonly language: string;
}
export class UpdateAppLanguageDto {
constructor(
public readonly isMaster: boolean,
public readonly isOptional: boolean,
public readonly fallback: string[]
) {
}
export interface UpdateAppLanguageDto {
readonly isMaster?: boolean;
readonly isOptional?: boolean;
readonly fallback?: string[];
}
@Injectable()

5
src/Squidex/app/shared/services/app-patterns.service.spec.ts

@ -14,7 +14,6 @@ import {
AppPatternDto,
AppPatternsDto,
AppPatternsService,
EditAppPatternDto,
Version
} from './../';
@ -80,7 +79,7 @@ describe('AppPatternsService', () => {
it('should make post request to add pattern',
inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => {
const dto = new EditAppPatternDto('Number', '[0-9]', 'Message1');
const dto = { name: 'Number', pattern: '[0-9]' };
let pattern: AppPatternDto;
@ -106,7 +105,7 @@ describe('AppPatternsService', () => {
it('should make put request to update pattern',
inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => {
const dto = new EditAppPatternDto('Number', '[0-9]', 'Message1');
const dto = { name: 'Number', pattern: '[0-9]' };
patternService.putPattern('my-app', '1', dto, version).subscribe();

18
src/Squidex/app/shared/services/app-patterns.service.ts

@ -20,7 +20,7 @@ import {
Versioned
} from '@app/framework';
export class AppPatternsDto extends Model {
export class AppPatternsDto extends Model<AppPatternsDto> {
constructor(
public readonly patterns: AppPatternDto[],
public readonly version: Version
@ -29,27 +29,23 @@ export class AppPatternsDto extends Model {
}
}
export class AppPatternDto extends Model {
export class AppPatternDto extends Model<AppPatternDto> {
constructor(
public readonly id: string,
public readonly name: string,
public readonly pattern: string,
public readonly message: string
public readonly message?: string
) {
super();
}
}
export class EditAppPatternDto {
constructor(
public readonly name: string,
public readonly pattern: string,
public readonly message: string
) {
}
export interface EditAppPatternDto {
readonly name: string;
readonly pattern: string;
readonly message?: string;
}
@Injectable()
export class AppPatternsService {
constructor(

6
src/Squidex/app/shared/services/app-roles.service.spec.ts

@ -14,8 +14,6 @@ import {
AppRoleDto,
AppRolesDto,
AppRolesService,
CreateAppRoleDto,
UpdateAppRoleDto,
Version
} from './../';
@ -101,7 +99,7 @@ describe('AppRolesService', () => {
it('should make post request to add role',
inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => {
const dto = new CreateAppRoleDto('Role3');
const dto = { name: 'Role3' };
let role: AppRoleDto;
@ -122,7 +120,7 @@ describe('AppRolesService', () => {
it('should make put request to update role',
inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => {
const dto = new UpdateAppRoleDto(['P4', 'P5']);
const dto = { permissions: ['P4', 'P5'] };
roleService.putRole('my-app', 'role1', dto, version).subscribe();

22
src/Squidex/app/shared/services/app-roles.service.ts

@ -20,7 +20,7 @@ import {
Versioned
} from '@app/framework';
export class AppRolesDto extends Model {
export class AppRolesDto extends Model<AppRolesDto> {
constructor(
public readonly roles: AppRoleDto[],
public readonly version: Version
@ -29,7 +29,7 @@ export class AppRolesDto extends Model {
}
}
export class AppRoleDto extends Model {
export class AppRoleDto extends Model<AppRoleDto> {
constructor(
public readonly name: string,
public readonly numClients: number,
@ -38,24 +38,14 @@ export class AppRoleDto extends Model {
) {
super();
}
public with(value: Partial<AppRoleDto>): AppRoleDto {
return this.clone(value);
}
}
export class CreateAppRoleDto {
constructor(
public readonly name: string
) {
}
export interface CreateAppRoleDto {
readonly name: string;
}
export class UpdateAppRoleDto {
constructor(
public readonly permissions: string[]
) {
}
export interface UpdateAppRoleDto {
readonly permissions: string[];
}
@Injectable()

3
src/Squidex/app/shared/services/apps.service.spec.ts

@ -13,7 +13,6 @@ import {
ApiUrlConfig,
AppDto,
AppsService,
CreateAppDto,
DateTime,
Permission
} from './../';
@ -83,7 +82,7 @@ describe('AppsService', () => {
it('should make post request to create app',
inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => {
const dto = new CreateAppDto('new-app');
const dto = { name: 'new-app' };
let app: AppDto;

15
src/Squidex/app/shared/services/apps.service.ts

@ -20,26 +20,23 @@ import {
pretifyError
} from '@app/framework';
export class AppDto extends Model {
export class AppDto extends Model<AppDto> {
constructor(
public readonly id: string,
public readonly name: string,
public readonly permissions: Permission[],
public readonly created: DateTime,
public readonly lastModified: DateTime,
public readonly planName: string,
public readonly planUpgrade: string
public readonly planName?: string,
public readonly planUpgrade?: string
) {
super();
}
}
export class CreateAppDto {
constructor(
public readonly name: string,
public readonly template?: string
) {
}
export interface CreateAppDto {
readonly name: string;
readonly template?: string;
}
@Injectable()

32
src/Squidex/app/shared/services/assets.service.spec.ts

@ -10,7 +10,6 @@ import { inject, TestBed } from '@angular/core/testing';
import {
AnalyticsService,
AnnotateAssetDto,
ApiUrlConfig,
AssetDto,
AssetReplacedDto,
@ -31,9 +30,9 @@ describe('AssetDto', () => {
const newVersion = new Version('2');
it('should update tag property and user info when annnoting', () => {
const update = new AnnotateAssetDto('NewName.png', null, null);
const update = { fileName: 'New-Name.png' };
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'name.png', [], 'url', version);
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'Hash', 'png', 1, 1, 'image/png', false, false, 1, 1, 'name.png', [], 'url', version);
const asset_2 = asset_1.annnotate(update, modifier, newVersion, modified);
expect(asset_2.fileName).toEqual('NewName.png');
@ -45,11 +44,12 @@ describe('AssetDto', () => {
});
it('should update file properties when uploading', () => {
const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2);
const update = new AssetReplacedDto('Hash New', 2, 2, 'image/jpeg', true, 2, 2);
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'name.png', [], 'url', version);
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'Hash', 'png', 1, 1, 'image/png', false, false, 1, 1, 'name.png', [], 'url', version);
const asset_2 = asset_1.update(update, modifier, newVersion, modified);
expect(asset_2.fileHash).toEqual('Hash New');
expect(asset_2.fileSize).toEqual(2);
expect(asset_2.fileVersion).toEqual(2);
expect(asset_2.mimeType).toEqual('image/jpeg');
@ -133,6 +133,7 @@ describe('AssetsService', () => {
lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1',
fileName: 'My Asset1.png',
fileHash: 'My Hash1',
fileType: 'png',
fileSize: 1024,
fileVersion: 2000,
@ -151,6 +152,7 @@ describe('AssetsService', () => {
lastModified: '2017-10-12T10:10',
lastModifiedBy: 'LastModifiedBy2',
fileName: 'My Asset2.png',
fileHash: 'My Hash1',
fileType: 'png',
fileSize: 1024,
fileVersion: 2000,
@ -172,10 +174,12 @@ describe('AssetsService', () => {
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
'My Asset1.png',
'My Hash1',
'png',
1024,
2000,
'image/png',
false,
true,
1024,
2048,
@ -187,10 +191,12 @@ describe('AssetsService', () => {
DateTime.parseISO_UTC('2016-10-12T10:10'),
DateTime.parseISO_UTC('2017-10-12T10:10'),
'My Asset2.png',
'My Hash1',
'png',
1024,
2000,
'image/png',
false,
true,
1024,
2048,
@ -222,6 +228,7 @@ describe('AssetsService', () => {
lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1',
fileName: 'My Asset1.png',
fileHash: 'My Hash1',
fileType: 'png',
fileSize: 1024,
fileVersion: 2000,
@ -243,10 +250,12 @@ describe('AssetsService', () => {
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
'My Asset1.png',
'My Hash1',
'png',
1024,
2000,
'image/png',
false,
true,
1024,
2048,
@ -312,10 +321,12 @@ describe('AssetsService', () => {
req.flush({
id: 'id1',
fileName: 'My Asset1.png',
fileHash: 'My Hash1',
fileType: 'png',
fileSize: 1024,
fileVersion: 2,
mimeType: 'image/png',
isDuplicate: true,
isImage: true,
pixelWidth: 1024,
pixelHeight: 2048,
@ -335,10 +346,12 @@ describe('AssetsService', () => {
now,
now,
'My Asset1.png',
'My Hash1',
'png',
1024, 2,
'image/png',
true,
true,
1024,
2048,
'my-asset1.png',
@ -385,8 +398,9 @@ describe('AssetsService', () => {
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({
fileHash: 'Hash New',
fileSize: 1024,
fileVersion: 2,
fileVersion: 12,
mimeType: 'image/png',
isImage: true,
pixelWidth: 1024,
@ -395,7 +409,9 @@ describe('AssetsService', () => {
expect(asset!).toEqual(
new AssetReplacedDto(
1024, 2,
'Hash New',
1024,
12,
'image/png',
true,
1024,
@ -428,7 +444,7 @@ describe('AssetsService', () => {
it('should make put request to annotate asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
const dto = new AnnotateAssetDto('My Asset.pdf', 'my-asset.pdf', ['tag1', 'tag2']);
const dto = { fileName: 'New-Name.png' };
assetsService.putAsset('my-app', '123', dto, version).subscribe();

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

@ -18,21 +18,15 @@ import {
HTTP,
Model,
pretifyError,
ResultSet,
Types,
Version,
Versioned
} from '@app/framework';
export class AssetsDto extends Model {
constructor(
public readonly total: number,
public readonly items: AssetDto[]
) {
super();
}
}
export class AssetsDto extends ResultSet<AssetDto> { }
export class AssetDto extends Model {
export class AssetDto extends Model<AssetDto> {
public get canPreview() {
return this.isImage || (this.mimeType === 'image/svg+xml' && this.fileSize < 100 * 1024);
}
@ -44,10 +38,12 @@ export class AssetDto extends Model {
public readonly created: DateTime,
public readonly lastModified: DateTime,
public readonly fileName: string,
public readonly fileHash: string,
public readonly fileType: string,
public readonly fileSize: number,
public readonly fileVersion: number,
public readonly mimeType: string,
public readonly isDuplicate: boolean,
public readonly isImage: boolean,
public readonly pixelWidth: number | null,
public readonly pixelHeight: number | null,
@ -59,12 +55,8 @@ export class AssetDto extends Model {
super();
}
public with(value: Partial<AssetDto>, validOnly = false): AssetDto {
return this.clone(value, validOnly);
}
public update(update: AssetReplacedDto, user: string, version: Version, now?: DateTime): AssetDto {
return this.with({
return this.clone({
...update,
lastModified: now || DateTime.now(),
lastModifiedBy: user,
@ -73,7 +65,7 @@ export class AssetDto extends Model {
}
public annnotate(update: AnnotateAssetDto, user: string, version: Version, now?: DateTime): AssetDto {
return this.with({
return this.clone({
...<any>update,
lastModified: now || DateTime.now(),
lastModifiedBy: user,
@ -82,25 +74,20 @@ export class AssetDto extends Model {
}
}
export class AnnotateAssetDto {
constructor(
public readonly fileName: string | null,
public readonly slug: string | null,
public readonly tags: string[] | null
) {
}
export interface AnnotateAssetDto {
readonly fileName?: string;
readonly slug?: string;
readonly tags?: string[];
}
export class AssetReplacedDto {
constructor(
public readonly fileSize: number,
public readonly fileVersion: number,
public readonly mimeType: string,
public readonly isImage: boolean,
public readonly pixelWidth: number | null,
public readonly pixelHeight: number | null
) {
}
export interface AssetReplacedDto {
readonly fileHash: string;
readonly fileSize: number;
readonly fileVersion: number;
readonly mimeType: string;
readonly isImage: boolean;
readonly pixelWidth?: number | null;
readonly pixelHeight?: number | null;
}
@Injectable()
@ -169,10 +156,12 @@ export class AssetsService {
DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified),
item.fileName,
item.fileHash,
item.fileType,
item.fileSize,
item.fileVersion,
item.mimeType,
false,
item.isImage,
item.pixelWidth,
item.pixelHeight,
@ -212,10 +201,12 @@ export class AssetsService {
now,
now,
response.fileName,
response.fileHash,
response.fileType,
response.fileSize,
response.fileVersion,
response.mimeType,
response.isDuplicate,
response.isImage,
response.pixelWidth,
response.pixelHeight,
@ -260,10 +251,12 @@ export class AssetsService {
DateTime.parseISO_UTC(body.created),
DateTime.parseISO_UTC(body.lastModified),
body.fileName,
body.fileHash,
body.fileType,
body.fileSize,
body.fileVersion,
body.mimeType,
false,
body.isImage,
body.pixelWidth,
body.pixelHeight,
@ -292,15 +285,7 @@ export class AssetsService {
} else if (Types.is(event, HttpResponse)) {
const response: any = event.body;
const replaced = new AssetReplacedDto(
response.fileSize,
response.fileVersion,
response.mimeType,
response.isImage,
response.pixelWidth,
response.pixelHeight);
return new Versioned(new Version(event.headers.get('etag')!), replaced);
return new Versioned(new Version(event.headers.get('etag')!), response);
} else {
throw 'Invalid';
}

7
src/Squidex/app/shared/services/backups.service.spec.ts

@ -14,8 +14,7 @@ import {
BackupDto,
BackupsService,
DateTime,
RestoreDto,
StartRestoreDto
RestoreDto
} from './../';
describe('BackupsService', () => {
@ -170,7 +169,9 @@ describe('BackupsService', () => {
it('should make post request to start restore',
inject([BackupsService, HttpTestingController], (backupsService: BackupsService, httpMock: HttpTestingController) => {
backupsService.postRestore(new StartRestoreDto('http://url')).subscribe();
const dto = { url: 'http://url' };
backupsService.postRestore(dto).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/restore');

14
src/Squidex/app/shared/services/backups.service.ts

@ -19,7 +19,7 @@ import {
Types
} from '@app/framework';
export class BackupDto extends Model {
export class BackupDto extends Model<BackupDto> {
constructor(
public readonly id: string,
public readonly started: DateTime,
@ -32,7 +32,7 @@ export class BackupDto extends Model {
}
}
export class RestoreDto {
export class RestoreDto extends Model<BackupDto> {
constructor(
public readonly url: string,
public readonly started: DateTime,
@ -40,15 +40,13 @@ export class RestoreDto {
public readonly status: string,
public readonly log: string[]
) {
super();
}
}
export class StartRestoreDto {
constructor(
public readonly url: string,
public readonly newAppName?: string
) {
}
export interface StartRestoreDto {
readonly url: string;
readonly newAppName?: string;
}
@Injectable()

9
src/Squidex/app/shared/services/comments.service.spec.ts

@ -14,7 +14,6 @@ import {
CommentsDto,
CommentsService,
DateTime,
UpsertCommentDto,
Version
} from './../';
@ -84,9 +83,11 @@ describe('CommentsService', () => {
it('should make post request to create comment',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
const dto = { text: 'text1' };
let comment: CommentDto;
commentsService.postComment('my-app', 'my-comments', new UpsertCommentDto('text1')).subscribe(result => {
commentsService.postComment('my-app', 'my-comments', dto).subscribe(result => {
comment = result;
});
@ -108,7 +109,9 @@ describe('CommentsService', () => {
it('should make put request to replace comment content',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
commentsService.putComment('my-app', 'my-comments', '123', new UpsertCommentDto('text1')).subscribe();
const dto = { text: 'text1' };
commentsService.putComment('my-app', 'my-comments', '123', dto).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments/123');

15
src/Squidex/app/shared/services/comments.service.ts

@ -18,7 +18,7 @@ import {
Version
} from '@app/framework';
export class CommentsDto extends Model {
export class CommentsDto extends Model<CommentsDto> {
constructor(
public readonly createdComments: CommentDto[],
public readonly updatedComments: CommentDto[],
@ -29,7 +29,7 @@ export class CommentsDto extends Model {
}
}
export class CommentDto extends Model {
export class CommentDto extends Model<CommentDto> {
constructor(
public readonly id: string,
public readonly time: DateTime,
@ -38,17 +38,10 @@ export class CommentDto extends Model {
) {
super();
}
public with(value: Partial<CommentDto>): CommentDto {
return this.clone(value);
}
}
export class UpsertCommentDto {
constructor(
public readonly text: string
) {
}
export interface UpsertCommentDto {
readonly text: string;
}
@Injectable()

21
src/Squidex/app/shared/services/contents.service.ts

@ -17,25 +17,12 @@ import {
HTTP,
Model,
pretifyError,
ResultSet,
Version,
Versioned
} from '@app/framework';
export class ContentsDto extends Model {
constructor(
public readonly total: number,
public readonly items: ContentDto[]
) {
super();
}
public with(value: Partial<ContentsDto>): ContentsDto {
return this.clone(value);
}}
export class ScheduleDto extends Model {
export class ScheduleDto extends Model<ScheduleDto> {
constructor(
public readonly status: string,
public readonly scheduledBy: string,
@ -45,7 +32,9 @@ export class ScheduleDto extends Model {
}
}
export class ContentDto extends Model {
export class ContentsDto extends ResultSet<ContentDto> { }
export class ContentDto extends Model<ContentDto> {
constructor(
public readonly id: string,
public readonly status: string,

5
src/Squidex/app/shared/services/plans.service.spec.ts

@ -11,7 +11,6 @@ import { inject, TestBed } from '@angular/core/testing';
import {
AnalyticsService,
ApiUrlConfig,
ChangePlanDto,
PlanChangedDto,
PlanDto,
PlansDto,
@ -101,7 +100,7 @@ describe('PlansService', () => {
it('should make put request to change plan',
inject([PlansService, HttpTestingController], (plansService: PlansService, httpMock: HttpTestingController) => {
const dto = new ChangePlanDto('enterprise');
const dto = { planId: 'enterprise' };
let planChanged: PlanChangedDto;
@ -116,6 +115,6 @@ describe('PlansService', () => {
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
expect(planChanged!).toEqual(new PlanChangedDto('my-url'));
expect(planChanged!).toEqual({ redirectUri: 'http://url' });
}));
});

20
src/Squidex/app/shared/services/plans.service.ts

@ -20,7 +20,7 @@ import {
Versioned
} from '@app/framework';
export class PlansDto extends Model {
export class PlansDto extends Model<PlansDto> {
constructor(
public readonly currentPlanId: string,
public readonly planOwner: string,
@ -32,7 +32,7 @@ export class PlansDto extends Model {
}
}
export class PlanDto extends Model {
export class PlanDto extends Model<PlanDto> {
constructor(
public readonly id: string,
public readonly name: string,
@ -47,18 +47,12 @@ export class PlanDto extends Model {
}
}
export class PlanChangedDto {
constructor(
public readonly redirectUri: string
) {
}
export interface PlanChangedDto {
readonly redirectUri?: string;
}
export class ChangePlanDto {
constructor(
public readonly planId: string
) {
}
export interface ChangePlanDto {
readonly planId: string;
}
@Injectable()
@ -106,7 +100,7 @@ export class PlansService {
map(response => {
const body = response.payload.body;
return new Versioned(response.version, new PlanChangedDto(body.redirectUri));
return new Versioned(response.version, body);
}),
tap(() => {
this.analytics.trackEvent('Plan', 'Changed', appName);

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

@ -12,7 +12,6 @@ import { inject, TestBed } from '@angular/core/testing';
import {
AnalyticsService,
ApiUrlConfig,
CreateRuleDto,
DateTime,
RuleDto,
RuleElementDto,
@ -20,7 +19,6 @@ import {
RuleEventDto,
RuleEventsDto,
RulesService,
UpdateRuleDto,
Version
} from './../';
@ -168,15 +166,18 @@ describe('RulesService', () => {
it('should make post request to create rule',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
const dto = new CreateRuleDto({
param1: 1,
param2: 2,
triggerType: 'ContentChanged'
}, {
param3: 3,
param4: 4,
actionType: 'Webhook'
});
const dto = {
trigger: {
param1: 1,
param2: 2,
triggerType: 'ContentChanged'
},
action: {
param3: 3,
param4: 4,
actionType: 'Webhook'
}
};
let rule: RuleDto;
@ -216,7 +217,14 @@ describe('RulesService', () => {
it('should make put request to update rule',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
const dto = new UpdateRuleDto({ param1: 1 }, { param2: 2 });
const dto = {
trigger: {
param1: 1
},
action: {
param3: 2
}
};
rulesService.putRule('my-app', '123', dto, version).subscribe();

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

@ -17,6 +17,7 @@ import {
HTTP,
Model,
pretifyError,
ResultSet,
Version,
Versioned
} from '@app/framework';
@ -72,7 +73,7 @@ export class RuleElementPropertyDto {
}
}
export class RuleDto extends Model {
export class RuleDto extends Model<RuleDto> {
constructor(
public readonly id: string,
public readonly createdBy: string,
@ -88,22 +89,11 @@ export class RuleDto extends Model {
) {
super();
}
public with(value: Partial<RuleDto>): RuleDto {
return this.clone(value);
}
}
export class RuleEventsDto extends Model {
constructor(
public readonly total: number,
public readonly items: RuleEventDto[]
) {
super();
}
}
export class RuleEventsDto extends ResultSet<RuleEventDto> { }
export class RuleEventDto extends Model {
export class RuleEventDto extends Model<RuleEventDto> {
constructor(
public readonly id: string,
public readonly created: DateTime,
@ -117,26 +107,16 @@ export class RuleEventDto extends Model {
) {
super();
}
public with(value: Partial<RuleEventDto>): RuleEventDto {
return this.clone(value);
}
}
export class CreateRuleDto {
constructor(
public readonly trigger: any,
public readonly action: any
) {
}
export interface CreateRuleDto {
readonly trigger: any;
readonly action: any;
}
export class UpdateRuleDto {
constructor(
public readonly trigger: any,
public readonly action: any
) {
}
export interface UpdateRuleDto {
readonly trigger?: any;
readonly action?: any;
}
@Injectable()

11
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -22,9 +22,6 @@ import {
SchemaDto,
SchemaPropertiesDto,
SchemasService,
UpdateFieldDto,
UpdateSchemaCategoryDto,
UpdateSchemaDto,
Version
} from './../';
@ -358,7 +355,7 @@ describe('SchemasService', () => {
it('should make put request to update schema',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateSchemaDto('label', 'hints');
const dto = { label: 'label1' };
schemasService.putSchema('my-app', 'my-schema', dto, version).subscribe();
@ -388,7 +385,7 @@ describe('SchemasService', () => {
it('should make put request to update category',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateSchemaCategoryDto();
const dto = {};
schemasService.putCategory('my-app', 'my-schema', dto, version).subscribe();
@ -486,7 +483,7 @@ describe('SchemasService', () => {
it('should make put request to update field',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateFieldDto(createProperties('Number'));
const dto = { properties: createProperties('Number') };
schemasService.putField('my-app', 'my-schema', 1, dto, undefined, version).subscribe();
@ -501,7 +498,7 @@ describe('SchemasService', () => {
it('should make put request to update nested field',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateFieldDto(createProperties('Number'));
const dto = { properties: createProperties('Number') };
schemasService.putField('my-app', 'my-schema', 1, dto, 13, version).subscribe();

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

@ -24,7 +24,7 @@ import {
import { createProperties, FieldPropertiesDto } from './schemas.types';
export class SchemaDto extends Model {
export class SchemaDto extends Model<SchemaDto> {
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label, this.name);
}
@ -44,10 +44,6 @@ export class SchemaDto extends Model {
) {
super();
}
public with(value: Partial<SchemaDto>): SchemaDto {
return this.clone(value);
}
}
export class SchemaDetailsDto extends SchemaDto {
@ -88,13 +84,9 @@ export class SchemaDetailsDto extends SchemaDto {
this.listFieldsEditable = fields.filter(x => x.isInlineEditable);
}
}
public with(value: Partial<SchemaDetailsDto>): SchemaDetailsDto {
return this.clone(value);
}
}
export class FieldDto extends Model {
export class FieldDto extends Model<FieldDto> {
public get isInlineEditable(): boolean {
return !this.isDisabled && this.properties['inlineEditable'] === true;
}
@ -117,10 +109,6 @@ export class FieldDto extends Model {
) {
super();
}
public with(value: Partial<FieldDto>): FieldDto {
return this.clone(value);
}
}
export class RootFieldDto extends FieldDto {
@ -149,10 +137,6 @@ export class RootFieldDto extends FieldDto {
) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled);
}
public with(value: Partial<RootFieldDto>): RootFieldDto {
return this.clone(value);
}
}
export class NestedFieldDto extends FieldDto {
@ -164,10 +148,6 @@ export class NestedFieldDto extends FieldDto {
) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled);
}
public with(value: Partial<NestedFieldDto>): NestedFieldDto {
return this.clone(value);
}
}
export class SchemaPropertiesDto {
@ -197,26 +177,17 @@ export class CreateSchemaDto {
}
}
export class UpdateSchemaCategoryDto {
constructor(
public readonly name?: string
) {
}
export interface UpdateSchemaCategoryDto {
readonly name?: string;
}
export class UpdateFieldDto {
constructor(
public readonly properties: FieldPropertiesDto
) {
}
export interface UpdateFieldDto {
readonly properties: FieldPropertiesDto;
}
export class UpdateSchemaDto {
constructor(
public readonly label?: string,
public readonly hints?: string
) {
}
export interface UpdateSchemaDto {
readonly label?: string;
readonly hints?: string;
}
@Injectable()

18
src/Squidex/app/shared/services/schemas.types.ts

@ -5,7 +5,19 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export const fieldTypes = [
export type FieldType =
'Array' |
'Assets' |
'Boolean' |
'DateTime' |
'Json' |
'Geolocation' |
'Number' |
'References' |
'String' |
'Tags';
export const fieldTypes: { type: FieldType, description: string }[] = [
{
type: 'String',
description: 'Titles, names, paragraphs.'
@ -41,7 +53,7 @@ export const fieldTypes = [
export const fieldInvariant = 'iv';
export function createProperties(fieldType: string, values: Object | null = null): FieldPropertiesDto {
export function createProperties(fieldType: FieldType, values: Object | null = null): FieldPropertiesDto {
let properties: FieldPropertiesDto;
switch (fieldType) {
@ -109,7 +121,7 @@ export interface FieldPropertiesVisitor<T> {
}
export abstract class FieldPropertiesDto {
public abstract fieldType: string;
public abstract fieldType: FieldType;
public readonly editorUrl?: string;
public readonly label?: string;

7
src/Squidex/app/shared/services/translations.service.spec.ts

@ -10,7 +10,6 @@ import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
TranslateDto,
TranslationDto,
TranslationsService
} from './../';
@ -35,11 +34,11 @@ describe('TranslationsService', () => {
it('should make post request to translate text',
inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => {
let translation: TranslationDto;
const dto = { text: 'Hello', sourceLanguage: 'en', targetLanguage: 'de' };
const request = new TranslateDto('Hello', 'en', 'de');
let translation: TranslationDto;
translationsService.translate('my-app', request).subscribe(result => {
translationsService.translate('my-app', dto).subscribe(result => {
translation = result;
});

11
src/Squidex/app/shared/services/translations.service.ts

@ -20,13 +20,10 @@ export class TranslationDto {
}
}
export class TranslateDto {
constructor(
public readonly text: string,
public readonly sourceLanguage: string,
public readonly targetLanguage: string
) {
}
export interface TranslateDto {
readonly text: string;
readonly sourceLanguage: string;
readonly targetLanguage: string;
}
@Injectable()

6
src/Squidex/app/shared/services/ui.service.ts

@ -13,10 +13,10 @@ import { catchError } from 'rxjs/operators';
import { ApiUrlConfig } from '@app/framework';
export interface UISettingsDto {
mapType: string;
mapKey?: string;
readonly mapType: string;
readonly mapKey?: string;
canCreateApps: boolean;
readonly canCreateApps: boolean;
}
@Injectable()

11
src/Squidex/app/shared/state/apps.state.spec.ts

@ -12,7 +12,6 @@ import {
AppDto,
AppsService,
AppsState,
CreateAppDto,
DateTime,
DialogService,
Permission
@ -22,11 +21,11 @@ describe('AppsState', () => {
const now = DateTime.now();
const oldApps = [
new AppDto('id1', 'old-name1', [new Permission('Owner')], now, now, 'Free', 'Plan'),
new AppDto('id2', 'old-name2', [new Permission('Owner')], now, now, 'Free', 'Plan')
new AppDto('id1', 'old-name1', [new Permission('Owner')], now, now),
new AppDto('id2', 'old-name2', [new Permission('Owner')], now, now)
];
const newApp = new AppDto('id3', 'new-name', [new Permission('Owner')], now, now, 'Free', 'Plan');
const newApp = new AppDto('id3', 'new-name', [new Permission('Owner')], now, now);
let dialogs: IMock<DialogService>;
let appsService: IMock<AppsService>;
@ -83,7 +82,7 @@ describe('AppsState', () => {
});
it('should add app to snapshot when created', () => {
const request = new CreateAppDto(newApp.name);
const request = { ...newApp };
appsService.setup(x => x.postApp(request))
.returns(() => of(newApp));
@ -94,7 +93,7 @@ describe('AppsState', () => {
});
it('should remove app from snashot when archived', () => {
const request = new CreateAppDto(newApp.name);
const request = { ...newApp };
appsService.setup(x => x.postApp(request))
.returns(() => of(newApp));

8
src/Squidex/app/shared/state/assets.state.spec.ts

@ -30,8 +30,8 @@ describe('AssetsState', () => {
const newVersion = new Version('2');
const oldAssets = [
new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, 'slug1', ['tag1', 'shared'], 'url1', version),
new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, 'slug2', ['tag2', 'shared'], 'url2', version)
new AssetDto('id1', creator, creator, creation, creation, 'name1', 'hash1', 'type1', 1, 1, 'mime1', false, false, null, null, 'slug1', ['tag1', 'shared'], 'url1', version),
new AssetDto('id2', creator, creator, creation, creation, 'name2', 'hash2', 'type2', 2, 2, 'mime2', false, false, null, null, 'slug2', ['tag2', 'shared'], 'url2', version)
];
let dialogs: IMock<DialogService>;
@ -81,7 +81,7 @@ describe('AssetsState', () => {
});
it('should add asset to snapshot when created', () => {
const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'slug2', [], 'url3', version);
const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug2', [], 'url3', version);
assetsState.add(newAsset);
@ -90,7 +90,7 @@ describe('AssetsState', () => {
});
it('should update properties when updated', () => {
const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'slug3', ['new'], 'url3', version);
const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug3', ['new'], 'url3', version);
assetsState.update(newAsset);

12
src/Squidex/app/shared/state/clients.state.spec.ts

@ -14,9 +14,7 @@ import {
AppClientsService,
AppsState,
ClientsState,
CreateAppClientDto,
DialogService,
UpdateAppClientDto,
Version,
Versioned
} from './../';
@ -27,8 +25,8 @@ describe('ClientsState', () => {
const newVersion = new Version('2');
const oldClients = [
new AppClientDto('id1', 'name1', 'secret1', 'Developer'),
new AppClientDto('id2', 'name2', 'secret2', 'Developer')
new AppClientDto('id1', 'name1', 'secret1'),
new AppClientDto('id2', 'name2', 'secret2')
];
let dialogs: IMock<DialogService>;
@ -70,9 +68,9 @@ describe('ClientsState', () => {
});
it('should add client to snapshot when created', () => {
const newClient = new AppClientDto('id3', 'name3', 'secret3', 'Developer');
const newClient = new AppClientDto('id3', 'name3', 'secret3');
const request = new CreateAppClientDto('id3');
const request = { id: 'id3' };
clientsService.setup(x => x.postClient(app, request, version))
.returns(() => of(new Versioned<AppClientDto>(newVersion, newClient)));
@ -84,7 +82,7 @@ describe('ClientsState', () => {
});
it('should update properties when updated', () => {
const request = new UpdateAppClientDto('NewName', 'NewRole');
const request = { name: 'NewName', role: 'NewRole' };
clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version))
.returns(() => of(new Versioned<any>(newVersion, {})));

19
src/Squidex/app/shared/state/comments.state.spec.ts

@ -17,7 +17,6 @@ import {
DateTime,
DialogService,
ImmutableArray,
UpsertCommentDto,
Version
} from './../';
@ -56,10 +55,10 @@ describe('CommentsState', () => {
it('should load and merge comments', () => {
const newComments = new CommentsDto([
new CommentDto('3', now, 'text3', user)
], [
new CommentDto('2', now, 'text2_2', user)
], ['1'], new Version('2'));
new CommentDto('3', now, 'text3', user)
], [
new CommentDto('2', now, 'text2_2', user)
], ['1'], new Version('2'));
commentsService.setup(x => x.getComments(app, commentsId, new Version('1')))
.returns(() => of(newComments));
@ -78,7 +77,9 @@ describe('CommentsState', () => {
it('should add comment to snapshot when created', () => {
const newComment = new CommentDto('3', now, 'text3', user);
commentsService.setup(x => x.postComment(app, commentsId, new UpsertCommentDto('text3')))
const request = { text: 'text3' };
commentsService.setup(x => x.postComment(app, commentsId, request))
.returns(() => of(newComment));
commentsState.create('text3').subscribe();
@ -91,7 +92,9 @@ describe('CommentsState', () => {
});
it('should update properties when updated', () => {
commentsService.setup(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2')))
const request = { text: 'text2_2' };
commentsService.setup(x => x.putComment(app, commentsId, '2', request))
.returns(() => of({}));
commentsState.update('2', 'text2_2', now).subscribe();
@ -101,7 +104,7 @@ describe('CommentsState', () => {
new CommentDto('2', now, 'text2_2', user)
]));
commentsService.verify(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2')), Times.once());
commentsService.verify(x => x.putComment(app, commentsId, '2', request), Times.once());
});
it('should remove comment from snapshot when deleted', () => {

11
src/Squidex/app/shared/state/comments.state.ts

@ -17,12 +17,7 @@ import {
Version
} from '@app/framework';
import {
CommentDto,
CommentsService,
UpsertCommentDto
} from './../services/comments.service';
import { CommentDto, CommentsService } from './../services/comments.service';
import { AppsState } from './apps.state';
interface Snapshot {
@ -81,7 +76,7 @@ export class CommentsState extends State<Snapshot> {
}
public create(text: string): Observable<any> {
return this.commentsService.postComment(this.appName, this.commentsId, new UpsertCommentDto(text)).pipe(
return this.commentsService.postComment(this.appName, this.commentsId, { text }).pipe(
tap(dto => {
this.next(s => {
const comments = s.comments.push(dto);
@ -93,7 +88,7 @@ export class CommentsState extends State<Snapshot> {
}
public update(commentId: string, text: string, now?: DateTime): Observable<any> {
return this.commentsService.putComment(this.appName, this.commentsId, commentId, new UpsertCommentDto(text)).pipe(
return this.commentsService.putComment(this.appName, this.commentsId, commentId, { text }).pipe(
tap(() => {
this.next(s => {
const comments = s.comments.map(c => c.id === commentId ? update(c, text, now || DateTime.now()) : c);

3
src/Squidex/app/shared/state/contributors.state.spec.ts

@ -13,7 +13,6 @@ import {
AppContributorsDto,
AppContributorsService,
AppsState,
AssignContributorDto,
AuthService,
ContributorAssignedDto,
ContributorsState,
@ -84,7 +83,7 @@ describe('ContributorsState', () => {
it('should add contributor to snapshot when assigned', () => {
const newContributor = new AppContributorDto('id3', 'Developer');
const request = new AssignContributorDto('mail2stehle@gmail.com', 'Developer');
const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' };
contributorsService.setup(x => x.postContributor(app, request, version))
.returns(() => of(new Versioned<ContributorAssignedDto>(newVersion, new ContributorAssignedDto('id3', true))));

3
src/Squidex/app/shared/state/languages.state.spec.ts

@ -18,7 +18,6 @@ import {
LanguageDto,
LanguagesService,
LanguagesState,
UpdateAppLanguageDto,
Version,
Versioned
} from './../';
@ -121,7 +120,7 @@ describe('LanguagesState', () => {
});
it('should update language in snapshot when updated', () => {
const request = new UpdateAppLanguageDto(true, false, []);
const request = { isMaster: true };
languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version))
.returns(() => of(new Versioned<any>(newVersion, {})));

22
src/Squidex/app/shared/state/languages.state.ts

@ -17,7 +17,12 @@ import {
Version
} from '@app/framework';
import { AddAppLanguageDto, AppLanguageDto, AppLanguagesService, UpdateAppLanguageDto } from './../services/app-languages.service';
import {
AppLanguageDto,
AppLanguagesService,
UpdateAppLanguageDto
} from './../services/app-languages.service';
import { LanguageDto, LanguagesService } from './../services/languages.service';
import { AppsState } from './apps.state';
@ -105,7 +110,7 @@ export class LanguagesState extends State<Snapshot> {
}
public add(language: LanguageDto): Observable<any> {
return this.appLanguagesService.postLanguage(this.appName, new AddAppLanguageDto(language.iso2Code), this.version).pipe(
return this.appLanguagesService.postLanguage(this.appName, { language: language.iso2Code }, this.version).pipe(
tap(dto => {
const languages = this.snapshot.plainLanguages.push(dto.payload).sortByStringAsc(x => x.englishName);
@ -129,9 +134,9 @@ export class LanguagesState extends State<Snapshot> {
tap(dto => {
const languages = this.snapshot.plainLanguages.map(l => {
if (l.iso2Code === language.iso2Code) {
return update(l, request.isMaster, request.isOptional, request.fallback);
return update(l, request);
} else if (l.isMaster && request.isMaster) {
return update(l, false, l.isOptional, l.fallback);
return update(l, { isMaster: false });
} else {
return l;
}
@ -190,10 +195,5 @@ export class LanguagesState extends State<Snapshot> {
}
}
const update = (language: AppLanguageDto, isMaster: boolean, isOptional: boolean, fallback: string[]) =>
new AppLanguageDto(
language.iso2Code,
language.englishName,
isMaster,
isOptional,
fallback);
const update = (language: AppLanguageDto, request: UpdateAppLanguageDto) =>
language.with(request);

11
src/Squidex/app/shared/state/patterns.state.spec.ts

@ -14,7 +14,6 @@ import {
AppPatternsService,
AppsState,
DialogService,
EditAppPatternDto,
PatternsState,
Version,
Versioned
@ -70,7 +69,7 @@ describe('PatternsState', () => {
it('should add pattern to snapshot when created', () => {
const newPattern = new AppPatternDto('id3', 'name3', 'pattern3', '');
const request = new EditAppPatternDto('name3', 'pattern3', '');
const request = { ...newPattern };
patternsService.setup(x => x.postPattern(app, request, version))
.returns(() => of(new Versioned<AppPatternDto>(newVersion, newPattern)));
@ -82,7 +81,7 @@ describe('PatternsState', () => {
});
it('should update properties when updated', () => {
const request = new EditAppPatternDto('a_name2', 'a_pattern2', 'a_message2');
const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' };
patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version))
.returns(() => of(new Versioned<any>(newVersion, {})));
@ -91,9 +90,9 @@ describe('PatternsState', () => {
const pattern_1 = patternsState.snapshot.patterns.at(0);
expect(pattern_1.name).toBe('a_name2');
expect(pattern_1.pattern).toBe('a_pattern2');
expect(pattern_1.message).toBe('a_message2');
expect(pattern_1.name).toBe(request.name);
expect(pattern_1.pattern).toBe(request.pattern);
expect(pattern_1.message).toBe(request.message);
expect(patternsState.snapshot.version).toEqual(newVersion);
});

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

@ -120,4 +120,4 @@ export class PatternsState extends State<Snapshot> {
}
const update = (pattern: AppPatternDto, request: EditAppPatternDto) =>
new AppPatternDto(pattern.id, request.name, request.pattern, request.message);
pattern.with(request);

4
src/Squidex/app/shared/state/plans.state.spec.ts

@ -103,7 +103,7 @@ describe('PlansState', () => {
plansState.window = <any>{ location: {} };
plansService.setup(x => x.putPlan(app, It.isAny(), version))
.returns(() => of(new Versioned<PlanChangedDto>(newVersion, new PlanChangedDto('URI'))));
.returns(() => of(new Versioned<PlanChangedDto>(newVersion, { redirectUri: 'http://url' })));
plansState.load().subscribe();
plansState.change('free').pipe(onErrorResumeNext()).subscribe();
@ -120,7 +120,7 @@ describe('PlansState', () => {
plansState.window = <any>{ location: {} };
plansService.setup(x => x.putPlan(app, It.isAny(), version))
.returns(() => of(new Versioned<PlanChangedDto>(newVersion, new PlanChangedDto(''))));
.returns(() => of(new Versioned<PlanChangedDto>(newVersion, { redirectUri: '' })));
plansState.load().subscribe();
plansState.change('id2_yearly').pipe(onErrorResumeNext()).subscribe();

9
src/Squidex/app/shared/state/plans.state.ts

@ -18,14 +18,9 @@ import {
} from '@app/framework';
import { AuthService } from './../services/auth.service';
import { PlanDto, PlansService } from './../services/plans.service';
import { AppsState } from './apps.state';
import {
ChangePlanDto,
PlanDto,
PlansService
} from './../services/plans.service';
interface PlanInfo {
// The plan.
plan: PlanDto;
@ -116,7 +111,7 @@ export class PlansState extends State<Snapshot> {
}
public change(planId: string): Observable<any> {
return this.plansService.putPlan(this.appName, new ChangePlanDto(planId), this.version).pipe(
return this.plansService.putPlan(this.appName, { planId }, this.version).pipe(
tap(dto => {
if (dto.payload.redirectUri && dto.payload.redirectUri.length > 0) {
this.window.location.href = dto.payload.redirectUri;

6
src/Squidex/app/shared/state/roles.state.spec.ts

@ -13,10 +13,8 @@ import {
AppRolesDto,
AppRolesService,
AppsState,
CreateAppRoleDto,
DialogService,
RolesState,
UpdateAppRoleDto,
Version,
Versioned
} from './../';
@ -72,7 +70,7 @@ describe('RolesState', () => {
it('should add role to snapshot when added', () => {
const newRole = new AppRoleDto('Role3', 0, 0, ['P3']);
const request = new CreateAppRoleDto('Role3');
const request = { name: newRole.name };
rolesService.setup(x => x.postRole(app, request, version))
.returns(() => of(new Versioned<AppRoleDto>(newVersion, newRole)));
@ -84,7 +82,7 @@ describe('RolesState', () => {
});
it('should update permissions when updated', () => {
const request = new UpdateAppRoleDto(['P4', 'P5']);
const request = { permissions: ['P4', 'P5'] };
rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version))
.returns(() => of(new Versioned<any>(newVersion, {})));

3
src/Squidex/app/shared/state/rules.state.spec.ts

@ -13,7 +13,6 @@ import { RulesState } from './rules.state';
import {
AppsState,
AuthService,
CreateRuleDto,
DateTime,
DialogService,
RuleDto,
@ -83,7 +82,7 @@ describe('RulesState', () => {
it('should add rule to snapshot when created', () => {
const newRule = new RuleDto('id3', creator, creator, creation, creation, version, false, {}, 'trigger3', {}, 'action3');
const request = new CreateRuleDto({}, {});
const request = { action: {}, trigger: {} };
rulesService.setup(x => x.postRule(app, request, modifier, creation))
.returns(() => of(newRule));

7
src/Squidex/app/shared/state/rules.state.ts

@ -24,8 +24,7 @@ import { AppsState } from './apps.state';
import {
CreateRuleDto,
RuleDto,
RulesService,
UpdateRuleDto
RulesService
} from './../services/rules.service';
interface Snapshot {
@ -100,7 +99,7 @@ export class RulesState extends State<Snapshot> {
}
public updateAction(rule: RuleDto, action: any, now?: DateTime): Observable<any> {
return this.rulesService.putRule(this.appName, rule.id, new UpdateRuleDto(null, action), rule.version).pipe(
return this.rulesService.putRule(this.appName, rule.id, { action }, rule.version).pipe(
tap(dto => {
this.replaceRule(updateAction(rule, action, this.user, dto.version, now));
}),
@ -108,7 +107,7 @@ export class RulesState extends State<Snapshot> {
}
public updateTrigger(rule: RuleDto, trigger: any, now?: DateTime): Observable<any> {
return this.rulesService.putRule(this.appName, rule.id, new UpdateRuleDto(trigger, null), rule.version).pipe(
return this.rulesService.putRule(this.appName, rule.id, { trigger }, rule.version).pipe(
tap(dto => {
this.replaceRule(updateTrigger(rule, trigger, this.user, dto.version, now));
}),

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
public string FileName { get; set; }
public string FileHash { get; set; }
public string Slug { get; set; }
public long FileSize { get; set; }

112
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -14,6 +14,7 @@ using FakeItEasy;
using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Entities.TestHelpers;
@ -26,8 +27,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetCommandMiddlewareTests : HandlerTestBase<AssetState>
{
private readonly AssetQueryService assetQueryService = A.Fake<AssetQueryService>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetStore assetStore = A.Fake<IAssetStore>();
private readonly IAssetStore assetStore = A.Fake<MemoryAssetStore>();
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly ITagGenerator<CreateAsset> tagGenerator = A.Fake<ITagGenerator<CreateAsset>>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
@ -56,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset);
sut = new AssetCommandMiddleware(grainFactory, assetStore, assetThumbnailGenerator, new[] { tagGenerator });
sut = new AssetCommandMiddleware(grainFactory, assetQueryService, assetStore, assetThumbnailGenerator, new[] { tagGenerator });
}
[Fact]
@ -65,20 +67,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
var command = new CreateAsset { AssetId = assetId, File = file };
var context = CreateContextForCommand(command);
A.CallTo(() => tagGenerator.GenerateTags(command, A<HashSet<string>>.Ignored))
.Invokes(new Action<CreateAsset, HashSet<string>>((c, tags) =>
{
tags.Add("tag1");
tags.Add("tag2");
}));
SetupTags(command);
SetupImageInfo();
await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>();
Assert.Equal(assetId, result.Id);
Assert.Equal(assetId, result.IdOrValue);
Assert.Contains("tag1", result.Tags);
Assert.Contains("tag2", result.Tags);
@ -86,10 +82,66 @@ namespace Squidex.Domain.Apps.Entities.Assets
AssertAssetImageChecked();
}
[Fact]
public async Task Create_should_calculate_hash()
{
var command = new CreateAsset { AssetId = assetId, File = file };
var context = CreateContextForCommand(command);
SetupImageInfo();
await sut.HandleAsync(context);
Assert.True(command.FileHash.Length > 10);
}
[Fact]
public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found()
{
var command = new CreateAsset { AssetId = assetId, File = file };
var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _);
SetupImageInfo();
await sut.HandleAsync(context);
Assert.True(context.Result<AssetCreatedResult>().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 context = CreateContextForCommand(command);
SetupSameHashAsset("other-name", file.FileSize, out _);
SetupImageInfo();
await sut.HandleAsync(context);
Assert.False(context.Result<AssetCreatedResult>().IsDuplicate);
}
[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 context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, 12345, out _);
SetupImageInfo();
await sut.HandleAsync(context);
Assert.False(context.Result<AssetCreatedResult>().IsDuplicate);
}
[Fact]
public async Task Update_should_update_domain_object()
{
var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file });
var command = new UpdateAsset { AssetId = assetId, File = file };
var context = CreateContextForCommand(command);
SetupImageInfo();
@ -101,16 +153,41 @@ namespace Squidex.Domain.Apps.Entities.Assets
AssertAssetImageChecked();
}
[Fact]
public async Task Update_should_calculate_hash()
{
var command = new UpdateAsset { AssetId = assetId, File = file };
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
Assert.True(command.FileHash.Length > 10);
}
private Task ExecuteCreateAsync()
{
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file }));
}
private void SetupTags(CreateAsset command)
{
A.CallTo(() => tagGenerator.GenerateTags(command, A<HashSet<string>>.Ignored))
.Invokes(new Action<CreateAsset, HashSet<string>>((c, tags) =>
{
tags.Add("tag1");
tags.Add("tag2");
}));
}
private void AssertAssetHasBeenUploaded(long version, Guid commitId)
{
var fileName = AssetStoreExtensions.GetFileName(assetId.ToString(), version);
A.CallTo(() => assetStore.UploadAsync(commitId.ToString(), stream, false, CancellationToken.None))
A.CallTo(() => assetStore.UploadAsync(commitId.ToString(), A<HasherStream>.Ignored, false, CancellationToken.None))
.MustHaveHappened();
A.CallTo(() => assetStore.CopyAsync(commitId.ToString(), fileName, CancellationToken.None))
.MustHaveHappened();
@ -118,6 +195,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
.MustHaveHappened();
}
private void SetupSameHashAsset(string fileName, long fileSize, out IAssetEntity existing)
{
var temp = existing = A.Fake<IAssetEntity>();
A.CallTo(() => temp.FileName).Returns(fileName);
A.CallTo(() => temp.FileSize).Returns(fileSize);
A.CallTo(() => assetQueryService.FindAssetByHashAsync(A<Guid>.Ignored, A<string>.Ignored))
.Returns(existing);
}
private void SetupImageInfo()
{
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream))

15
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs

@ -27,8 +27,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly Guid assetId = Guid.NewGuid();
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
private readonly Guid assetId = Guid.NewGuid();
private readonly string fileHash = Guid.NewGuid().ToString();
private readonly AssetGrain sut;
protected override Guid Id
@ -57,13 +58,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact]
public async Task Create_should_create_events()
{
var command = new CreateAsset { File = file, ImageInfo = image };
var command = new CreateAsset { File = file, ImageInfo = image, FileHash = fileHash, Tags = new HashSet<string>() };
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(new AssetSavedResult(0, 0));
result.ShouldBeEquivalent(new AssetSavedResult(0, 0, fileHash));
Assert.Equal(0, sut.Snapshot.FileVersion);
Assert.Equal(fileHash, sut.Snapshot.FileHash);
LastEvents
.ShouldHaveSameEvents(
@ -71,6 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
IsImage = true,
FileName = file.FileName,
FileHash = fileHash,
FileSize = file.FileSize,
FileVersion = 0,
MimeType = file.MimeType,
@ -85,15 +88,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact]
public async Task Update_should_create_events()
{
var command = new UpdateAsset { File = file, ImageInfo = image };
var command = new UpdateAsset { File = file, ImageInfo = image, FileHash = fileHash };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(new AssetSavedResult(1, 1));
result.ShouldBeEquivalent(new AssetSavedResult(1, 1, fileHash));
Assert.Equal(1, sut.Snapshot.FileVersion);
Assert.Equal(fileHash, sut.Snapshot.FileHash);
LastEvents
.ShouldHaveSameEvents(
@ -101,6 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
IsImage = true,
FileSize = file.FileSize,
FileHash = fileHash,
FileVersion = 1,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,

15
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs

@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var id = Guid.NewGuid();
A.CallTo(() => assetRepository.FindAssetAsync(id))
A.CallTo(() => assetRepository.FindAssetAsync(id, false))
.Returns(CreateAsset(id, "id1", "id2", "id3"));
var result = await sut.FindAssetAsync(context, id);
@ -65,6 +65,19 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags);
}
[Fact]
public async Task Should_find_asset_by_hash_and_resolve_tags()
{
var id = Guid.NewGuid();
A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash"))
.Returns(CreateAsset(id, "id1", "id2", "id3"));
var result = await sut.FindAssetByHashAsync(appId.Id, "hash");
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags);
}
[Fact]
public async Task Should_load_assets_from_ids_and_resolve_tags()
{

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs

@ -37,6 +37,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
public string FileName { get; set; }
public string FileHash { get; set; }
public string Slug { get; set; }
public long FileSize { get; set; }

47
tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs

@ -0,0 +1,47 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Security.Cryptography;
using Xunit;
namespace Squidex.Infrastructure.Assets
{
public class HasherStreamTests
{
[Fact]
public void Should_calculate_hash_while_copying()
{
var source = GenerateTestData();
var sourceHash = source.Sha256Base64();
var sourceStream = new HasherStream(new MemoryStream(source), HashAlgorithmName.SHA256);
using (sourceStream)
{
var target = new MemoryStream();
sourceStream.CopyTo(target);
var targetHash = sourceStream.GetHashStringAndReset();
Assert.Equal(sourceHash, targetHash);
}
}
private byte[] GenerateTestData(int length = 1000)
{
var random = new Random();
var result = new byte[length];
random.NextBytes(result);
return result;
}
}
}

10
tests/Squidex.Infrastructure.Tests/DispatchingTests.cs

@ -10,6 +10,8 @@ using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks;
using Xunit;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Infrastructure
{
public class DispatchingTests
@ -95,13 +97,13 @@ namespace Squidex.Infrastructure
public Task On(MyEventA @event, int context)
{
EventATriggered = EventATriggered + context;
EventATriggered += context;
return TaskHelper.Done;
}
public Task On(MyEventB @event, int context)
{
EventBTriggered = EventATriggered + context;
EventBTriggered += context;
return TaskHelper.Done;
}
}
@ -169,12 +171,12 @@ namespace Squidex.Infrastructure
public void On(MyEventA @event, int context)
{
EventATriggered = EventATriggered + context;
EventATriggered += context;
}
public void On(MyEventB @event, int context)
{
EventBTriggered = EventATriggered + context;
EventBTriggered += context;
}
}

Loading…
Cancel
Save