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 FileName { get; }
string FileHash { get; }
string Slug { 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] [BsonElement]
public string FileName { get; set; } public string FileName { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public string FileHash { get; set; }
[BsonIgnoreIfDefault] [BsonIgnoreIfDefault]
[BsonElement] [BsonElement]
public string Slug { get; set; } 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) .Ascending(x => x.Tags)
.Descending(x => x.LastModified)), .Descending(x => x.LastModified)),
new CreateIndexModel<MongoAssetEntity>( 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); 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>()) using (Profiler.TraceMethod<MongoAssetRepository>())
{ {
var assetEntity = var assetEntity =
await Collection.Find(x => x.Slug == slug) await Collection.Find(x => x.IndexedAppId == appId && x.Slug == slug)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true)
{
return null;
}
return assetEntity; 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>()) using (Profiler.TraceMethod<MongoAssetRepository>())
{ {
@ -122,6 +151,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.Id == id) await Collection.Find(x => x.Id == id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true && !allowDeleted)
{
return null;
}
return assetEntity; 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.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Apps.State namespace Squidex.Domain.Apps.Entities.Apps.State
{ {
[CollectionName("Apps")] [CollectionName("Apps")]

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

@ -7,9 +7,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
@ -20,21 +22,25 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain> public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain>
{ {
private readonly IAssetStore assetStore; private readonly IAssetStore assetStore;
private readonly AssetQueryService assetQueryService;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators; private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
public AssetCommandMiddleware( public AssetCommandMiddleware(
IGrainFactory grainFactory, IGrainFactory grainFactory,
AssetQueryService assetQueryService,
IAssetStore assetStore, IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators) IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
: base(grainFactory) : base(grainFactory)
{ {
Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQueryService, nameof(assetQueryService));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators)); Guard.NotNull(tagGenerators, nameof(tagGenerators));
this.assetStore = assetStore; this.assetStore = assetStore;
this.assetQueryService = assetQueryService;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators; this.tagGenerators = tagGenerators;
@ -53,21 +59,45 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead()); createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead());
foreach (var tagGenerator in tagGenerators) createAsset.FileHash = await UploadAsync(context, createAsset.File);
{
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
var originalTags = new HashSet<string>(createAsset.Tags);
await assetStore.UploadAsync(context.ContextId.ToString(), createAsset.File.OpenRead());
try try
{ {
var result = (AssetSavedResult)await ExecuteCommandAsync(createAsset); var existing = await assetQueryService.FindAssetByHashAsync(createAsset.AppId.Id, createAsset.FileHash);
context.Complete(new AssetCreatedResult(createAsset.AssetId, originalTags, result.Version)); 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 finally
{ {
@ -81,10 +111,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead()); 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 try
{ {
var result = await ExecuteCommandAsync(updateAsset) as AssetSavedResult; var result = (AssetSavedResult)await ExecuteCommandAsync(updateAsset);
context.Complete(result); context.Complete(result);
@ -103,5 +133,24 @@ namespace Squidex.Domain.Apps.Entities.Assets
break; 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 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 HashSet<string> Tags { get; }
public AssetCreatedResult(Guid id, HashSet<string> tags, long version) public long FileVersion { get; }
: base(version)
{ public string FileHash { get; }
Id = id;
public bool IsDuplicate { get; }
public AssetCreatedResult(Guid id, HashSet<string> tags, long version, long fileVersion, string fileHash, bool isDuplicate)
: base(id, version)
{
Tags = tags; 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); 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: case UpdateAsset updateRule:
return UpdateAsync(updateRule, c => return UpdateAsync(updateRule, c =>
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
Update(c); Update(c);
return new AssetSavedResult(Version, Snapshot.FileVersion); return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash);
}); });
case DeleteAsset deleteAsset: case DeleteAsset deleteAsset:
return UpdateAsync(deleteAsset, async c => return UpdateAsync(deleteAsset, async c =>
@ -76,12 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug); GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
if (c.Tags != null) var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
{
c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
}
Annotate(c); Annotate(c, tagIds);
}); });
default: default:
throw new NotSupportedException(); throw new NotSupportedException();
@ -90,32 +87,37 @@ namespace Squidex.Domain.Apps.Entities.Assets
private async Task<HashSet<string>> NormalizeTagsAsync(Guid appId, HashSet<string> tags) 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); var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags);
return new HashSet<string>(normalized.Values); 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 var @event = SimpleMapper.Map(command, new AssetCreated
{ {
IsImage = command.ImageInfo != null,
FileName = command.File.FileName, FileName = command.File.FileName,
FileSize = command.File.FileSize, FileSize = command.File.FileSize,
FileVersion = 0, FileVersion = 0,
MimeType = command.File.MimeType, MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth, PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight, PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null,
Slug = command.File.FileName.ToAssetSlug() Slug = command.File.FileName.ToAssetSlug()
}); });
@event.Tags = tagIds;
RaiseEvent(@event); RaiseEvent(@event);
} }
public void Update(UpdateAsset command) public void Update(UpdateAsset command)
{ {
VerifyNotDeleted();
var @event = SimpleMapper.Map(command, new AssetUpdated var @event = SimpleMapper.Map(command, new AssetUpdated
{ {
FileVersion = Snapshot.FileVersion + 1, FileVersion = Snapshot.FileVersion + 1,
@ -129,14 +131,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
RaiseEvent(@event); 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) 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 namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetQueryService : IAssetQueryService public class AssetQueryService : IAssetQueryService
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
@ -38,21 +38,38 @@ namespace Squidex.Domain.Apps.Entities.Assets
this.tagService = tagService; 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)); 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); var asset = await assetRepository.FindAssetAsync(id);
if (asset != null) 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; 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(context, nameof(context));
Guard.NotNull(query, nameof(query)); 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 long FileVersion { get; }
public AssetSavedResult(long version, long fileVersion) public string FileHash { get; }
public AssetSavedResult(long version, long fileVersion, string fileHash)
: base(version) : base(version)
{ {
FileVersion = fileVersion; 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 HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset() public CreateAsset()
{ {
AssetId = Guid.NewGuid(); 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 AssetFile File { get; set; }
public ImageInfo ImageInfo { 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<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); 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.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Assets.State namespace Squidex.Domain.Apps.Entities.Assets.State
{ {
public class AssetState : DomainObjectState<AssetState>, IAssetEntity public class AssetState : DomainObjectState<AssetState>, IAssetEntity
@ -26,6 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[DataMember] [DataMember]
public string FileName { get; set; } public string FileName { get; set; }
[DataMember]
public string FileHash { get; set; }
[DataMember] [DataMember]
public string MimeType { get; set; } 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.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Contents.State namespace Squidex.Domain.Apps.Entities.Contents.State
{ {
public class ContentState : DomainObjectState<ContentState>, IContentEntity 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.EventSourcing;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Rules.State namespace Squidex.Domain.Apps.Entities.Rules.State
{ {
[CollectionName("Rules")] [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.EventSourcing;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Schemas.State namespace Squidex.Domain.Apps.Entities.Schemas.State
{ {
[CollectionName("Schemas")] [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 FileName { get; set; }
public string FileHash { get; set; }
public string MimeType { get; set; } public string MimeType { get; set; }
public string Slug { 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 MimeType { get; set; }
public string FileHash { get; set; }
public long FileSize { get; set; } public long FileSize { get; set; }
public long FileVersion { 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 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 ConcurrentDictionary<string, MemoryStream> streams = new ConcurrentDictionary<string, MemoryStream>();
private readonly AsyncLock readerLock = new AsyncLock(); private readonly AsyncLock readerLock = new AsyncLock();
@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Assets
return null; 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(sourceFileName, nameof(sourceFileName));
Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); 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)); 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)); 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)); Guard.NotNullOrEmpty(fileName, nameof(fileName));

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

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

8
src/Squidex.Infrastructure/RandomHash.cs

@ -19,11 +19,15 @@ namespace Squidex.Infrastructure
} }
public static string Sha256Base64(this string value) 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()) using (var sha = SHA256.Create())
{ {
var bytesValue = Encoding.UTF8.GetBytes(value); var bytesHash = sha.ComputeHash(bytes);
var bytesHash = sha.ComputeHash(bytesValue);
var result = Convert.ToBase64String(bytesHash); 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}")] [Route("assets/{id}/{*more}")]
[ProducesResponseType(typeof(FileResult), 200)] [ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0.5)] [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] long version = EtagVersion.Any,
[FromQuery] int? width = null, [FromQuery] int? width = null,
[FromQuery] int? height = null, [FromQuery] int? height = null,
@ -71,15 +102,20 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
IAssetEntity entity; IAssetEntity entity;
if (Guid.TryParse(id, out var guid)) if (Guid.TryParse(idOrSlug, out var guid))
{ {
entity = await assetRepository.FindAssetAsync(guid); entity = await assetRepository.FindAssetAsync(guid);
} }
else 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) if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0)
{ {
return NotFound(); return NotFound();

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

@ -76,6 +76,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary> /// </summary>
public int? PixelHeight { get; set; } public int? PixelHeight { get; set; }
/// <summary>
/// Indicates if the asset has been already uploaded.
/// </summary>
public bool IsDuplicate { get; set; }
/// <summary> /// <summary>
/// The version of the asset. /// The version of the asset.
/// </summary> /// </summary>
@ -83,22 +88,21 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
public static AssetCreatedDto FromCommand(CreateAsset command, AssetCreatedResult result) 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, FileName = command.File.FileName,
FileSize = command.File.FileSize, FileSize = command.File.FileSize,
FileType = command.File.FileName.FileType(), FileType = command.File.FileName.FileType(),
FileVersion = result.Version, FileVersion = result.FileVersion,
MimeType = command.File.MimeType, MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null, IsImage = command.ImageInfo != null,
IsDuplicate = result.IsDuplicate,
PixelWidth = command.ImageInfo?.PixelWidth, PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight, PixelHeight = command.ImageInfo?.PixelHeight,
Tags = result.Tags, Tags = result.Tags,
Version = result.Version 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] [Required]
public string FileName { get; set; } public string FileName { get; set; }
/// <summary>
/// The file hash.
/// </summary>
[Required]
public string FileHash { get; set; }
/// <summary> /// <summary>
/// The slug. /// The slug.
/// </summary> /// </summary>

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

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

2
src/Squidex/Squidex.csproj

@ -153,6 +153,6 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605</NoWarn> <NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060</NoWarn>
</PropertyGroup> </PropertyGroup>
</Project> </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 './services/users.service';
export * from './state/event-consumers.state'; export * from './state/event-consumers.state';
export * from './state/users.forms';
export * from './state/users.state'; 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 { export class EventConsumersPageComponent extends ResourceOwner implements OnInit {
public eventConsumerErrorDialog = new DialogModel(); public eventConsumerErrorDialog = new DialogModel();
public eventConsumerError = ''; public eventConsumerError?: string;
constructor( constructor(
public readonly eventConsumersState: EventConsumersState public readonly eventConsumersState: EventConsumersState

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

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

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

@ -13,25 +13,19 @@ import { map } from 'rxjs/operators';
import { import {
ApiUrlConfig, ApiUrlConfig,
Model, Model,
pretifyError pretifyError,
ResultSet
} from '@app/shared'; } from '@app/shared';
export class UsersDto extends Model { export class UsersDto extends ResultSet<UserDto> {}
constructor(
public readonly total: number,
public readonly items: UserDto[]
) {
super();
}
}
export class UserDto extends Model { export class UserDto extends Model<UserDto> {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly email: string, public readonly email: string,
public readonly displayName: string, public readonly displayName: string,
public readonly permissions: string[], public readonly permissions: string[] = [],
public readonly isLocked: boolean public readonly isLocked?: boolean
) { ) {
super(); super();
} }
@ -41,24 +35,18 @@ export class UserDto extends Model {
} }
} }
export class CreateUserDto { export interface CreateUserDto {
constructor( readonly email: string;
public readonly email: string, readonly displayName: string;
public readonly displayName: string, readonly permissions: string[];
public readonly permissions: string[], readonly password: string;
public readonly password: string
) {
}
} }
export class UpdateUserDto { export interface UpdateUserDto {
constructor( readonly email: string;
public readonly email: string, readonly displayName: string;
public readonly displayName: string, readonly permissions: string[];
public readonly permissions: string[], readonly password?: string;
public readonly password?: string
) {
}
} }
@Injectable() @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', () => { describe('EventConsumersState', () => {
const oldConsumers = [ const oldConsumers = [
new EventConsumerDto('name1', false, false, 'error', '1'), new EventConsumerDto('name1', false),
new EventConsumerDto('name2', true, true, 'error', '2') new EventConsumerDto('name2', true)
]; ];
let dialogs: IMock<DialogService>; 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 { UsersState } from './users.state';
import { import {
CreateUserDto,
UpdateUserDto,
UserDto, UserDto,
UsersDto, UsersDto,
UsersService UsersService
@ -168,7 +166,7 @@ describe('UsersState', () => {
}); });
it('should update user properties when updated', () => { 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)) usersService.setup(x => x.putUser('id1', request))
.returns(() => of({})); .returns(() => of({}));
@ -178,13 +176,14 @@ describe('UsersState', () => {
const user_1 = usersState.snapshot.users.at(0); const user_1 = usersState.snapshot.users.at(0);
expect(user_1.user.email).toEqual('new@mail.com'); expect(user_1.user.email).toEqual(request.email);
expect(user_1.user.displayName).toEqual('New'); expect(user_1.user.displayName).toEqual(request.permissions);
expect(user_1.user.permissions).toEqual(request.permissions);
expect(user_1).toBe(usersState.snapshot.selectedUser!); expect(user_1).toBe(usersState.snapshot.selectedUser!);
}); });
it('should add user to snapshot when created', () => { 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)) usersService.setup(x => x.postUser(request))
.returns(() => of(newUser)); .returns(() => of(newUser));

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

@ -6,7 +6,6 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
@ -15,12 +14,10 @@ import '@app/framework/utils/rxjs-extensions';
import { import {
AuthService, AuthService,
DialogService, DialogService,
Form,
ImmutableArray, ImmutableArray,
notify, notify,
Pager, Pager,
State, State
ValidatorsEx
} from '@app/shared'; } from '@app/shared';
import { import {
@ -30,61 +27,6 @@ import {
UsersService UsersService
} from './../services/users.service'; } 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 { interface SnapshotUser {
// The user. // The user.
user: UserDto; user: UserDto;

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

@ -97,9 +97,17 @@ export class Form<T extends AbstractControl> {
} }
} }
export class Model { export function createModel<T>(c: { new(): T; }, values: Partial<T>): T {
protected clone(update: ((v: any) => object) | object, validOnly = false): any { return Object.assign(new c(), values);
let values: object; }
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)) { if (Types.isFunction(update)) {
values = update(<any>this); values = update(<any>this);
} else { } 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 {}> { export class State<T extends {}> {
private readonly state: BehaviorSubject<Readonly<T>>; private readonly state: BehaviorSubject<Readonly<T>>;
private readonly initialState: 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 { import {
AssetDto, AssetDto,
AssetsState, AssetsState,
DialogService,
ImmutableArray ImmutableArray
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -38,10 +39,19 @@ export class AssetsListComponent {
@Output() @Output()
public select = new EventEmitter<AssetDto>(); public select = new EventEmitter<AssetDto>();
constructor(
private readonly dialogs: DialogService
) {
}
public add(file: File, asset: AssetDto) { public add(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file); 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() { public search() {

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

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

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

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

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

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

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

@ -20,7 +20,7 @@ import {
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export class AppContributorsDto extends Model { export class AppContributorsDto extends Model<AppContributorsDto> {
constructor( constructor(
public readonly contributors: AppContributorDto[], public readonly contributors: AppContributorDto[],
public readonly maxContributors: number, 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( constructor(
public readonly contributorId: string, public readonly contributorId: string,
public readonly role: string, public readonly role: string
public readonly invite = false
) { ) {
super(); super();
} }
} }
export class AppContributorDto extends Model { export interface ContributorAssignedDto {
constructor( readonly contributorId: string;
public readonly contributorId: string, readonly isCreated?: boolean;
public readonly role: string
) {
super();
}
} }
export class ContributorAssignedDto { export interface AssignContributorDto {
constructor( readonly contributorId: string;
public readonly contributorId: string, readonly role: string;
public readonly isCreated: boolean readonly invite?: boolean;
) {
}
} }
@Injectable() @Injectable()
@ -93,9 +86,7 @@ export class AppContributorsService {
map(response => { map(response => {
const body: any = response.payload.body; const body: any = response.payload.body;
const result = new ContributorAssignedDto(body.contributorId, body.isCreated); return new Versioned(response.version, body);
return new Versioned(response.version, result);
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Contributor', 'Configured', appName); 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 { inject, TestBed } from '@angular/core/testing';
import { import {
AddAppLanguageDto,
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
AppLanguageDto, AppLanguageDto,
AppLanguagesDto, AppLanguagesDto,
AppLanguagesService, AppLanguagesService,
UpdateAppLanguageDto,
Version Version
} from './../'; } from './../';
@ -83,7 +81,7 @@ describe('AppLanguagesService', () => {
it('should make post request to add language', it('should make post request to add language',
inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => {
const dto = new AddAppLanguageDto('de'); const dto = { language: 'de' };
let language: AppLanguageDto; let language: AppLanguageDto;
@ -104,7 +102,7 @@ describe('AppLanguagesService', () => {
it('should make put request to make master language', it('should make put request to make master language',
inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { 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(); appLanguagesService.putLanguage('my-app', 'de', dto, version).subscribe();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -14,8 +14,7 @@ import {
BackupDto, BackupDto,
BackupsService, BackupsService,
DateTime, DateTime,
RestoreDto, RestoreDto
StartRestoreDto
} from './../'; } from './../';
describe('BackupsService', () => { describe('BackupsService', () => {
@ -170,7 +169,9 @@ describe('BackupsService', () => {
it('should make post request to start restore', it('should make post request to start restore',
inject([BackupsService, HttpTestingController], (backupsService: BackupsService, httpMock: HttpTestingController) => { 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'); 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 Types
} from '@app/framework'; } from '@app/framework';
export class BackupDto extends Model { export class BackupDto extends Model<BackupDto> {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly started: DateTime, public readonly started: DateTime,
@ -32,7 +32,7 @@ export class BackupDto extends Model {
} }
} }
export class RestoreDto { export class RestoreDto extends Model<BackupDto> {
constructor( constructor(
public readonly url: string, public readonly url: string,
public readonly started: DateTime, public readonly started: DateTime,
@ -40,15 +40,13 @@ export class RestoreDto {
public readonly status: string, public readonly status: string,
public readonly log: string[] public readonly log: string[]
) { ) {
super();
} }
} }
export class StartRestoreDto { export interface StartRestoreDto {
constructor( readonly url: string;
public readonly url: string, readonly newAppName?: string;
public readonly newAppName?: string
) {
}
} }
@Injectable() @Injectable()

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

@ -14,7 +14,6 @@ import {
CommentsDto, CommentsDto,
CommentsService, CommentsService,
DateTime, DateTime,
UpsertCommentDto,
Version Version
} from './../'; } from './../';
@ -84,9 +83,11 @@ describe('CommentsService', () => {
it('should make post request to create comment', it('should make post request to create comment',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
const dto = { text: 'text1' };
let comment: CommentDto; 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; comment = result;
}); });
@ -108,7 +109,9 @@ describe('CommentsService', () => {
it('should make put request to replace comment content', it('should make put request to replace comment content',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { 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'); 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 Version
} from '@app/framework'; } from '@app/framework';
export class CommentsDto extends Model { export class CommentsDto extends Model<CommentsDto> {
constructor( constructor(
public readonly createdComments: CommentDto[], public readonly createdComments: CommentDto[],
public readonly updatedComments: 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( constructor(
public readonly id: string, public readonly id: string,
public readonly time: DateTime, public readonly time: DateTime,
@ -38,17 +38,10 @@ export class CommentDto extends Model {
) { ) {
super(); super();
} }
public with(value: Partial<CommentDto>): CommentDto {
return this.clone(value);
}
} }
export class UpsertCommentDto { export interface UpsertCommentDto {
constructor( readonly text: string;
public readonly text: string
) {
}
} }
@Injectable() @Injectable()

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

@ -17,25 +17,12 @@ import {
HTTP, HTTP,
Model, Model,
pretifyError, pretifyError,
ResultSet,
Version, Version,
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export class ContentsDto extends Model { export class ScheduleDto extends Model<ScheduleDto> {
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 {
constructor( constructor(
public readonly status: string, public readonly status: string,
public readonly scheduledBy: 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( constructor(
public readonly id: string, public readonly id: string,
public readonly status: 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 { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
ChangePlanDto,
PlanChangedDto, PlanChangedDto,
PlanDto, PlanDto,
PlansDto, PlansDto,
@ -101,7 +100,7 @@ describe('PlansService', () => {
it('should make put request to change plan', it('should make put request to change plan',
inject([PlansService, HttpTestingController], (plansService: PlansService, httpMock: HttpTestingController) => { inject([PlansService, HttpTestingController], (plansService: PlansService, httpMock: HttpTestingController) => {
const dto = new ChangePlanDto('enterprise'); const dto = { planId: 'enterprise' };
let planChanged: PlanChangedDto; let planChanged: PlanChangedDto;
@ -116,6 +115,6 @@ describe('PlansService', () => {
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value); 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 Versioned
} from '@app/framework'; } from '@app/framework';
export class PlansDto extends Model { export class PlansDto extends Model<PlansDto> {
constructor( constructor(
public readonly currentPlanId: string, public readonly currentPlanId: string,
public readonly planOwner: 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( constructor(
public readonly id: string, public readonly id: string,
public readonly name: string, public readonly name: string,
@ -47,18 +47,12 @@ export class PlanDto extends Model {
} }
} }
export class PlanChangedDto { export interface PlanChangedDto {
constructor( readonly redirectUri?: string;
public readonly redirectUri: string
) {
}
} }
export class ChangePlanDto { export interface ChangePlanDto {
constructor( readonly planId: string;
public readonly planId: string
) {
}
} }
@Injectable() @Injectable()
@ -106,7 +100,7 @@ export class PlansService {
map(response => { map(response => {
const body = response.payload.body; const body = response.payload.body;
return new Versioned(response.version, new PlanChangedDto(body.redirectUri)); return new Versioned(response.version, body);
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Plan', 'Changed', appName); 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 { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
CreateRuleDto,
DateTime, DateTime,
RuleDto, RuleDto,
RuleElementDto, RuleElementDto,
@ -20,7 +19,6 @@ import {
RuleEventDto, RuleEventDto,
RuleEventsDto, RuleEventsDto,
RulesService, RulesService,
UpdateRuleDto,
Version Version
} from './../'; } from './../';
@ -168,15 +166,18 @@ describe('RulesService', () => {
it('should make post request to create rule', it('should make post request to create rule',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
const dto = new CreateRuleDto({ const dto = {
param1: 1, trigger: {
param2: 2, param1: 1,
triggerType: 'ContentChanged' param2: 2,
}, { triggerType: 'ContentChanged'
param3: 3, },
param4: 4, action: {
actionType: 'Webhook' param3: 3,
}); param4: 4,
actionType: 'Webhook'
}
};
let rule: RuleDto; let rule: RuleDto;
@ -216,7 +217,14 @@ describe('RulesService', () => {
it('should make put request to update rule', it('should make put request to update rule',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { 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(); rulesService.putRule('my-app', '123', dto, version).subscribe();

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

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

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

@ -22,9 +22,6 @@ import {
SchemaDto, SchemaDto,
SchemaPropertiesDto, SchemaPropertiesDto,
SchemasService, SchemasService,
UpdateFieldDto,
UpdateSchemaCategoryDto,
UpdateSchemaDto,
Version Version
} from './../'; } from './../';
@ -358,7 +355,7 @@ describe('SchemasService', () => {
it('should make put request to update schema', it('should make put request to update schema',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { 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(); schemasService.putSchema('my-app', 'my-schema', dto, version).subscribe();
@ -388,7 +385,7 @@ describe('SchemasService', () => {
it('should make put request to update category', it('should make put request to update category',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateSchemaCategoryDto(); const dto = {};
schemasService.putCategory('my-app', 'my-schema', dto, version).subscribe(); schemasService.putCategory('my-app', 'my-schema', dto, version).subscribe();
@ -486,7 +483,7 @@ describe('SchemasService', () => {
it('should make put request to update field', it('should make put request to update field',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { 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(); 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', it('should make put request to update nested field',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { 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(); 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'; import { createProperties, FieldPropertiesDto } from './schemas.types';
export class SchemaDto extends Model { export class SchemaDto extends Model<SchemaDto> {
public get displayName() { public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label, this.name); return StringHelper.firstNonEmpty(this.properties.label, this.name);
} }
@ -44,10 +44,6 @@ export class SchemaDto extends Model {
) { ) {
super(); super();
} }
public with(value: Partial<SchemaDto>): SchemaDto {
return this.clone(value);
}
} }
export class SchemaDetailsDto extends SchemaDto { export class SchemaDetailsDto extends SchemaDto {
@ -88,13 +84,9 @@ export class SchemaDetailsDto extends SchemaDto {
this.listFieldsEditable = fields.filter(x => x.isInlineEditable); 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 { public get isInlineEditable(): boolean {
return !this.isDisabled && this.properties['inlineEditable'] === true; return !this.isDisabled && this.properties['inlineEditable'] === true;
} }
@ -117,10 +109,6 @@ export class FieldDto extends Model {
) { ) {
super(); super();
} }
public with(value: Partial<FieldDto>): FieldDto {
return this.clone(value);
}
} }
export class RootFieldDto extends FieldDto { export class RootFieldDto extends FieldDto {
@ -149,10 +137,6 @@ export class RootFieldDto extends FieldDto {
) { ) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled); super(fieldId, name, properties, isLocked, isHidden, isDisabled);
} }
public with(value: Partial<RootFieldDto>): RootFieldDto {
return this.clone(value);
}
} }
export class NestedFieldDto extends FieldDto { export class NestedFieldDto extends FieldDto {
@ -164,10 +148,6 @@ export class NestedFieldDto extends FieldDto {
) { ) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled); super(fieldId, name, properties, isLocked, isHidden, isDisabled);
} }
public with(value: Partial<NestedFieldDto>): NestedFieldDto {
return this.clone(value);
}
} }
export class SchemaPropertiesDto { export class SchemaPropertiesDto {
@ -197,26 +177,17 @@ export class CreateSchemaDto {
} }
} }
export class UpdateSchemaCategoryDto { export interface UpdateSchemaCategoryDto {
constructor( readonly name?: string;
public readonly name?: string
) {
}
} }
export class UpdateFieldDto { export interface UpdateFieldDto {
constructor( readonly properties: FieldPropertiesDto;
public readonly properties: FieldPropertiesDto
) {
}
} }
export class UpdateSchemaDto { export interface UpdateSchemaDto {
constructor( readonly label?: string;
public readonly label?: string, readonly hints?: string;
public readonly hints?: string
) {
}
} }
@Injectable() @Injectable()

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

@ -5,7 +5,19 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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', type: 'String',
description: 'Titles, names, paragraphs.' description: 'Titles, names, paragraphs.'
@ -41,7 +53,7 @@ export const fieldTypes = [
export const fieldInvariant = 'iv'; 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; let properties: FieldPropertiesDto;
switch (fieldType) { switch (fieldType) {
@ -109,7 +121,7 @@ export interface FieldPropertiesVisitor<T> {
} }
export abstract class FieldPropertiesDto { export abstract class FieldPropertiesDto {
public abstract fieldType: string; public abstract fieldType: FieldType;
public readonly editorUrl?: string; public readonly editorUrl?: string;
public readonly label?: 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 { import {
ApiUrlConfig, ApiUrlConfig,
TranslateDto,
TranslationDto, TranslationDto,
TranslationsService TranslationsService
} from './../'; } from './../';
@ -35,11 +34,11 @@ describe('TranslationsService', () => {
it('should make post request to translate text', it('should make post request to translate text',
inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => { 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; translation = result;
}); });

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

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

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

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

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

@ -12,7 +12,6 @@ import {
AppDto, AppDto,
AppsService, AppsService,
AppsState, AppsState,
CreateAppDto,
DateTime, DateTime,
DialogService, DialogService,
Permission Permission
@ -22,11 +21,11 @@ describe('AppsState', () => {
const now = DateTime.now(); const now = DateTime.now();
const oldApps = [ const oldApps = [
new AppDto('id1', 'old-name1', [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, 'Free', 'Plan') 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 dialogs: IMock<DialogService>;
let appsService: IMock<AppsService>; let appsService: IMock<AppsService>;
@ -83,7 +82,7 @@ describe('AppsState', () => {
}); });
it('should add app to snapshot when created', () => { it('should add app to snapshot when created', () => {
const request = new CreateAppDto(newApp.name); const request = { ...newApp };
appsService.setup(x => x.postApp(request)) appsService.setup(x => x.postApp(request))
.returns(() => of(newApp)); .returns(() => of(newApp));
@ -94,7 +93,7 @@ describe('AppsState', () => {
}); });
it('should remove app from snashot when archived', () => { it('should remove app from snashot when archived', () => {
const request = new CreateAppDto(newApp.name); const request = { ...newApp };
appsService.setup(x => x.postApp(request)) appsService.setup(x => x.postApp(request))
.returns(() => of(newApp)); .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 newVersion = new Version('2');
const oldAssets = [ const oldAssets = [
new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, 'slug1', ['tag1', 'shared'], 'url1', 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', 'type2', 2, 2, 'mime2', false, null, null, 'slug2', ['tag2', 'shared'], 'url2', 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>; let dialogs: IMock<DialogService>;
@ -81,7 +81,7 @@ describe('AssetsState', () => {
}); });
it('should add asset to snapshot when created', () => { 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); assetsState.add(newAsset);
@ -90,7 +90,7 @@ describe('AssetsState', () => {
}); });
it('should update properties when updated', () => { 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); assetsState.update(newAsset);

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

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

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

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

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

@ -17,12 +17,7 @@ import {
Version Version
} from '@app/framework'; } from '@app/framework';
import { import { CommentDto, CommentsService } from './../services/comments.service';
CommentDto,
CommentsService,
UpsertCommentDto
} from './../services/comments.service';
import { AppsState } from './apps.state'; import { AppsState } from './apps.state';
interface Snapshot { interface Snapshot {
@ -81,7 +76,7 @@ export class CommentsState extends State<Snapshot> {
} }
public create(text: string): Observable<any> { 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 => { tap(dto => {
this.next(s => { this.next(s => {
const comments = s.comments.push(dto); 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> { 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(() => { tap(() => {
this.next(s => { this.next(s => {
const comments = s.comments.map(c => c.id === commentId ? update(c, text, now || DateTime.now()) : c); 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, AppContributorsDto,
AppContributorsService, AppContributorsService,
AppsState, AppsState,
AssignContributorDto,
AuthService, AuthService,
ContributorAssignedDto, ContributorAssignedDto,
ContributorsState, ContributorsState,
@ -84,7 +83,7 @@ describe('ContributorsState', () => {
it('should add contributor to snapshot when assigned', () => { it('should add contributor to snapshot when assigned', () => {
const newContributor = new AppContributorDto('id3', 'Developer'); 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)) contributorsService.setup(x => x.postContributor(app, request, version))
.returns(() => of(new Versioned<ContributorAssignedDto>(newVersion, new ContributorAssignedDto('id3', true)))); .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, LanguageDto,
LanguagesService, LanguagesService,
LanguagesState, LanguagesState,
UpdateAppLanguageDto,
Version, Version,
Versioned Versioned
} from './../'; } from './../';
@ -121,7 +120,7 @@ describe('LanguagesState', () => {
}); });
it('should update language in snapshot when updated', () => { 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)) languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version))
.returns(() => of(new Versioned<any>(newVersion, {}))); .returns(() => of(new Versioned<any>(newVersion, {})));

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

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

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

@ -14,7 +14,6 @@ import {
AppPatternsService, AppPatternsService,
AppsState, AppsState,
DialogService, DialogService,
EditAppPatternDto,
PatternsState, PatternsState,
Version, Version,
Versioned Versioned
@ -70,7 +69,7 @@ describe('PatternsState', () => {
it('should add pattern to snapshot when created', () => { it('should add pattern to snapshot when created', () => {
const newPattern = new AppPatternDto('id3', 'name3', 'pattern3', ''); const newPattern = new AppPatternDto('id3', 'name3', 'pattern3', '');
const request = new EditAppPatternDto('name3', 'pattern3', ''); const request = { ...newPattern };
patternsService.setup(x => x.postPattern(app, request, version)) patternsService.setup(x => x.postPattern(app, request, version))
.returns(() => of(new Versioned<AppPatternDto>(newVersion, newPattern))); .returns(() => of(new Versioned<AppPatternDto>(newVersion, newPattern)));
@ -82,7 +81,7 @@ describe('PatternsState', () => {
}); });
it('should update properties when updated', () => { 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)) patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version))
.returns(() => of(new Versioned<any>(newVersion, {}))); .returns(() => of(new Versioned<any>(newVersion, {})));
@ -91,9 +90,9 @@ describe('PatternsState', () => {
const pattern_1 = patternsState.snapshot.patterns.at(0); const pattern_1 = patternsState.snapshot.patterns.at(0);
expect(pattern_1.name).toBe('a_name2'); expect(pattern_1.name).toBe(request.name);
expect(pattern_1.pattern).toBe('a_pattern2'); expect(pattern_1.pattern).toBe(request.pattern);
expect(pattern_1.message).toBe('a_message2'); expect(pattern_1.message).toBe(request.message);
expect(patternsState.snapshot.version).toEqual(newVersion); 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) => 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: {} }; plansState.window = <any>{ location: {} };
plansService.setup(x => x.putPlan(app, It.isAny(), version)) 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.load().subscribe();
plansState.change('free').pipe(onErrorResumeNext()).subscribe(); plansState.change('free').pipe(onErrorResumeNext()).subscribe();
@ -120,7 +120,7 @@ describe('PlansState', () => {
plansState.window = <any>{ location: {} }; plansState.window = <any>{ location: {} };
plansService.setup(x => x.putPlan(app, It.isAny(), version)) 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.load().subscribe();
plansState.change('id2_yearly').pipe(onErrorResumeNext()).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'; } from '@app/framework';
import { AuthService } from './../services/auth.service'; import { AuthService } from './../services/auth.service';
import { PlanDto, PlansService } from './../services/plans.service';
import { AppsState } from './apps.state'; import { AppsState } from './apps.state';
import {
ChangePlanDto,
PlanDto,
PlansService
} from './../services/plans.service';
interface PlanInfo { interface PlanInfo {
// The plan. // The plan.
plan: PlanDto; plan: PlanDto;
@ -116,7 +111,7 @@ export class PlansState extends State<Snapshot> {
} }
public change(planId: string): Observable<any> { 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 => { tap(dto => {
if (dto.payload.redirectUri && dto.payload.redirectUri.length > 0) { if (dto.payload.redirectUri && dto.payload.redirectUri.length > 0) {
this.window.location.href = dto.payload.redirectUri; this.window.location.href = dto.payload.redirectUri;

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

@ -13,10 +13,8 @@ import {
AppRolesDto, AppRolesDto,
AppRolesService, AppRolesService,
AppsState, AppsState,
CreateAppRoleDto,
DialogService, DialogService,
RolesState, RolesState,
UpdateAppRoleDto,
Version, Version,
Versioned Versioned
} from './../'; } from './../';
@ -72,7 +70,7 @@ describe('RolesState', () => {
it('should add role to snapshot when added', () => { it('should add role to snapshot when added', () => {
const newRole = new AppRoleDto('Role3', 0, 0, ['P3']); 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)) rolesService.setup(x => x.postRole(app, request, version))
.returns(() => of(new Versioned<AppRoleDto>(newVersion, newRole))); .returns(() => of(new Versioned<AppRoleDto>(newVersion, newRole)));
@ -84,7 +82,7 @@ describe('RolesState', () => {
}); });
it('should update permissions when updated', () => { 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)) rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version))
.returns(() => of(new Versioned<any>(newVersion, {}))); .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 { import {
AppsState, AppsState,
AuthService, AuthService,
CreateRuleDto,
DateTime, DateTime,
DialogService, DialogService,
RuleDto, RuleDto,
@ -83,7 +82,7 @@ describe('RulesState', () => {
it('should add rule to snapshot when created', () => { it('should add rule to snapshot when created', () => {
const newRule = new RuleDto('id3', creator, creator, creation, creation, version, false, {}, 'trigger3', {}, 'action3'); 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)) rulesService.setup(x => x.postRule(app, request, modifier, creation))
.returns(() => of(newRule)); .returns(() => of(newRule));

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

@ -24,8 +24,7 @@ import { AppsState } from './apps.state';
import { import {
CreateRuleDto, CreateRuleDto,
RuleDto, RuleDto,
RulesService, RulesService
UpdateRuleDto
} from './../services/rules.service'; } from './../services/rules.service';
interface Snapshot { interface Snapshot {
@ -100,7 +99,7 @@ export class RulesState extends State<Snapshot> {
} }
public updateAction(rule: RuleDto, action: any, now?: DateTime): Observable<any> { 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 => { tap(dto => {
this.replaceRule(updateAction(rule, action, this.user, dto.version, now)); 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> { 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 => { tap(dto => {
this.replaceRule(updateTrigger(rule, trigger, this.user, dto.version, now)); 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 FileName { get; set; }
public string FileHash { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public long FileSize { 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 Orleans;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -26,8 +27,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public class AssetCommandMiddlewareTests : HandlerTestBase<AssetState> public class AssetCommandMiddlewareTests : HandlerTestBase<AssetState>
{ {
private readonly AssetQueryService assetQueryService = A.Fake<AssetQueryService>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); 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 ITagService tagService = A.Fake<ITagService>();
private readonly ITagGenerator<CreateAsset> tagGenerator = A.Fake<ITagGenerator<CreateAsset>>(); private readonly ITagGenerator<CreateAsset> tagGenerator = A.Fake<ITagGenerator<CreateAsset>>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
@ -56,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset); .Returns(asset);
sut = new AssetCommandMiddleware(grainFactory, assetStore, assetThumbnailGenerator, new[] { tagGenerator }); sut = new AssetCommandMiddleware(grainFactory, assetQueryService, assetStore, assetThumbnailGenerator, new[] { tagGenerator });
} }
[Fact] [Fact]
@ -65,20 +67,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
var command = new CreateAsset { AssetId = assetId, File = file }; var command = new CreateAsset { AssetId = assetId, File = file };
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
A.CallTo(() => tagGenerator.GenerateTags(command, A<HashSet<string>>.Ignored)) SetupTags(command);
.Invokes(new Action<CreateAsset, HashSet<string>>((c, tags) =>
{
tags.Add("tag1");
tags.Add("tag2");
}));
SetupImageInfo(); SetupImageInfo();
await sut.HandleAsync(context); await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>(); var result = context.Result<AssetCreatedResult>();
Assert.Equal(assetId, result.Id); Assert.Equal(assetId, result.IdOrValue);
Assert.Contains("tag1", result.Tags); Assert.Contains("tag1", result.Tags);
Assert.Contains("tag2", result.Tags); Assert.Contains("tag2", result.Tags);
@ -86,10 +82,66 @@ namespace Squidex.Domain.Apps.Entities.Assets
AssertAssetImageChecked(); 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] [Fact]
public async Task Update_should_update_domain_object() 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(); SetupImageInfo();
@ -101,16 +153,41 @@ namespace Squidex.Domain.Apps.Entities.Assets
AssertAssetImageChecked(); 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() private Task ExecuteCreateAsync()
{ {
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file })); 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) private void AssertAssetHasBeenUploaded(long version, Guid commitId)
{ {
var fileName = AssetStoreExtensions.GetFileName(assetId.ToString(), version); 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(); .MustHaveHappened();
A.CallTo(() => assetStore.CopyAsync(commitId.ToString(), fileName, CancellationToken.None)) A.CallTo(() => assetStore.CopyAsync(commitId.ToString(), fileName, CancellationToken.None))
.MustHaveHappened(); .MustHaveHappened();
@ -118,6 +195,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
.MustHaveHappened(); .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() private void SetupImageInfo()
{ {
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) 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 ITagService tagService = A.Fake<ITagService>();
private readonly ImageInfo image = new ImageInfo(2048, 2048); 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 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; private readonly AssetGrain sut;
protected override Guid Id protected override Guid Id
@ -57,13 +58,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Create_should_create_events() 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)); 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(0, sut.Snapshot.FileVersion);
Assert.Equal(fileHash, sut.Snapshot.FileHash);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -71,6 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
IsImage = true, IsImage = true,
FileName = file.FileName, FileName = file.FileName,
FileHash = fileHash,
FileSize = file.FileSize, FileSize = file.FileSize,
FileVersion = 0, FileVersion = 0,
MimeType = file.MimeType, MimeType = file.MimeType,
@ -85,15 +88,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Update_should_create_events() 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(); await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetCommand(command)); 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(1, sut.Snapshot.FileVersion);
Assert.Equal(fileHash, sut.Snapshot.FileHash);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -101,6 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
IsImage = true, IsImage = true,
FileSize = file.FileSize, FileSize = file.FileSize,
FileHash = fileHash,
FileVersion = 1, FileVersion = 1,
MimeType = file.MimeType, MimeType = file.MimeType,
PixelWidth = image.PixelWidth, 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(); var id = Guid.NewGuid();
A.CallTo(() => assetRepository.FindAssetAsync(id)) A.CallTo(() => assetRepository.FindAssetAsync(id, false))
.Returns(CreateAsset(id, "id1", "id2", "id3")); .Returns(CreateAsset(id, "id1", "id2", "id3"));
var result = await sut.FindAssetAsync(context, id); 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); 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] [Fact]
public async Task Should_load_assets_from_ids_and_resolve_tags() 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 FileName { get; set; }
public string FileHash { get; set; }
public string Slug { get; set; } public string Slug { get; set; }
public long FileSize { 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 Squidex.Infrastructure.Tasks;
using Xunit; using Xunit;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {
public class DispatchingTests public class DispatchingTests
@ -95,13 +97,13 @@ namespace Squidex.Infrastructure
public Task On(MyEventA @event, int context) public Task On(MyEventA @event, int context)
{ {
EventATriggered = EventATriggered + context; EventATriggered += context;
return TaskHelper.Done; return TaskHelper.Done;
} }
public Task On(MyEventB @event, int context) public Task On(MyEventB @event, int context)
{ {
EventBTriggered = EventATriggered + context; EventBTriggered += context;
return TaskHelper.Done; return TaskHelper.Done;
} }
} }
@ -169,12 +171,12 @@ namespace Squidex.Infrastructure
public void On(MyEventA @event, int context) public void On(MyEventA @event, int context)
{ {
EventATriggered = EventATriggered + context; EventATriggered += context;
} }
public void On(MyEventB @event, int context) public void On(MyEventB @event, int context)
{ {
EventBTriggered = EventATriggered + context; EventBTriggered += context;
} }
} }

Loading…
Cancel
Save