mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
94 changed files with 2078 additions and 556 deletions
@ -0,0 +1,105 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Tags; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets |
|||
{ |
|||
public sealed class AssetQueryService : IAssetQueryService |
|||
{ |
|||
private readonly ITagService tagService; |
|||
private readonly IAssetRepository assetRepository; |
|||
|
|||
public AssetQueryService(ITagService tagService, IAssetRepository assetRepository) |
|||
{ |
|||
Guard.NotNull(tagService, nameof(tagService)); |
|||
Guard.NotNull(assetRepository, nameof(assetRepository)); |
|||
|
|||
this.tagService = tagService; |
|||
|
|||
this.assetRepository = assetRepository; |
|||
} |
|||
|
|||
public async Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
var asset = await assetRepository.FindAssetAsync(id); |
|||
|
|||
if (asset != null) |
|||
{ |
|||
await DenormalizeTagsAsync(context.App.Id, Enumerable.Repeat(asset, 1)); |
|||
} |
|||
|
|||
return asset; |
|||
} |
|||
|
|||
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Query query) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
Guard.NotNull(query, nameof(query)); |
|||
|
|||
IResultList<IAssetEntity> assets; |
|||
|
|||
if (query.Ids != null) |
|||
{ |
|||
assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<Guid>(query.Ids)); |
|||
assets = Sort(assets, query.Ids); |
|||
} |
|||
else |
|||
{ |
|||
assets = await assetRepository.QueryAsync(context.App.Id, query.ODataQuery); |
|||
} |
|||
|
|||
await DenormalizeTagsAsync(context.App.Id, assets); |
|||
|
|||
return assets; |
|||
} |
|||
|
|||
private IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IList<Guid> ids) |
|||
{ |
|||
var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null); |
|||
|
|||
return ResultList.Create(assets.Total, sorted); |
|||
} |
|||
|
|||
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets) |
|||
{ |
|||
var tags = new HashSet<string>(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct()); |
|||
|
|||
var tagsById = await tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, tags); |
|||
|
|||
foreach (var asset in assets) |
|||
{ |
|||
if (asset.Tags?.Count > 0) |
|||
{ |
|||
var tagNames = asset.Tags.ToList(); |
|||
|
|||
asset.Tags.Clear(); |
|||
|
|||
foreach (var id in tagNames) |
|||
{ |
|||
if (tagsById.TryGetValue(id, out var name)) |
|||
{ |
|||
asset.Tags.Add(name); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
asset.Tags?.Clear(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets.Commands |
|||
{ |
|||
public sealed class TagAsset : AssetCommand |
|||
{ |
|||
public HashSet<string> Tags { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets |
|||
{ |
|||
public interface IAssetQueryService |
|||
{ |
|||
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Query query); |
|||
|
|||
Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public sealed class ContentQueryContext : Cloneable<ContentQueryContext> |
|||
{ |
|||
public string SchemaIdOrName { get; private set; } |
|||
|
|||
public QueryContext Base { get; private set; } |
|||
|
|||
public ContentQueryContext(QueryContext @base) |
|||
{ |
|||
Guard.NotNull(@base, nameof(@base)); |
|||
|
|||
Base = @base; |
|||
} |
|||
|
|||
public ContentQueryContext WithSchemaName(string name) |
|||
{ |
|||
return Clone(c => c.SchemaIdOrName = name); |
|||
} |
|||
|
|||
public ContentQueryContext WithArchived(bool archived) |
|||
{ |
|||
return Clone(c => c.Base = c.Base.WithArchived(archived)); |
|||
} |
|||
|
|||
public ContentQueryContext WithFlatten(bool flatten) |
|||
{ |
|||
return Clone(c => c.Base = c.Base.WithFlatten(flatten)); |
|||
} |
|||
|
|||
public ContentQueryContext WithSchemaId(Guid id) |
|||
{ |
|||
return Clone(c => c.SchemaIdOrName = id.ToString()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities |
|||
{ |
|||
public interface IEntityWithTags |
|||
{ |
|||
HashSet<string> Tags { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities |
|||
{ |
|||
public sealed class Query : Cloneable<Query> |
|||
{ |
|||
public static readonly Query Empty = new Query(); |
|||
|
|||
public List<Guid> Ids { get; private set; } |
|||
|
|||
public string ODataQuery { get; private set; } |
|||
|
|||
public Query WithODataQuery(string odataQuery) |
|||
{ |
|||
return Clone(c => c.ODataQuery = odataQuery); |
|||
} |
|||
|
|||
public Query WithIds(IEnumerable<Guid> ids) |
|||
{ |
|||
return Clone(c => c.Ids = ids.ToList()); |
|||
} |
|||
|
|||
public Query WithIds(string ids) |
|||
{ |
|||
if (!string.IsNullOrEmpty(ids)) |
|||
{ |
|||
return Clone(c => |
|||
{ |
|||
c.Ids = new List<Guid>(); |
|||
|
|||
foreach (var id in ids.Split(',')) |
|||
{ |
|||
if (Guid.TryParse(id, out var guid)) |
|||
{ |
|||
c.Ids.Add(guid); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Tags |
|||
{ |
|||
public sealed class GrainTagService : ITagService |
|||
{ |
|||
private readonly IGrainFactory grainFactory; |
|||
|
|||
public GrainTagService(IGrainFactory grainFactory) |
|||
{ |
|||
Guard.NotNull(grainFactory, nameof(grainFactory)); |
|||
|
|||
this.grainFactory = grainFactory; |
|||
} |
|||
|
|||
public Task<HashSet<string>> NormalizeTagsAsync(Guid appId, string category, HashSet<string> names, HashSet<string> ids) |
|||
{ |
|||
return GetGrain(appId, category).NormalizeTagsAsync(names, ids); |
|||
} |
|||
|
|||
public Task<HashSet<string>> GetTagIdsAsync(Guid appId, string category, HashSet<string> names) |
|||
{ |
|||
return GetGrain(appId, category).GetTagIdsAsync(names); |
|||
} |
|||
|
|||
public Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string category, HashSet<string> ids) |
|||
{ |
|||
return GetGrain(appId, category).DenormalizeTagsAsync(ids); |
|||
} |
|||
|
|||
public Task<Dictionary<string, int>> GetTagsAsync(Guid appId, string category) |
|||
{ |
|||
return GetGrain(appId, category).GetTagsAsync(); |
|||
} |
|||
|
|||
private ITagGrain GetGrain(Guid appId, string category) |
|||
{ |
|||
Guard.NotNullOrEmpty(category, nameof(category)); |
|||
|
|||
return grainFactory.GetGrain<ITagGrain>($"{appId}_{category}"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Tags |
|||
{ |
|||
public interface ITagGrain : IGrainWithStringKey |
|||
{ |
|||
Task<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids); |
|||
|
|||
Task<HashSet<string>> GetTagIdsAsync(HashSet<string> names); |
|||
|
|||
Task<Dictionary<string, string>> DenormalizeTagsAsync(HashSet<string> ids); |
|||
|
|||
Task<Dictionary<string, int>> GetTagsAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Tags |
|||
{ |
|||
public interface ITagService |
|||
{ |
|||
Task<HashSet<string>> NormalizeTagsAsync(Guid appId, string category, HashSet<string> names, HashSet<string> ids); |
|||
|
|||
Task<HashSet<string>> GetTagIdsAsync(Guid appId, string category, HashSet<string> names); |
|||
|
|||
Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string category, HashSet<string> ids); |
|||
|
|||
Task<Dictionary<string, int>> GetTagsAsync(Guid appId, string category); |
|||
} |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Tags |
|||
{ |
|||
public sealed class TagGrain : GrainOfString, ITagGrain |
|||
{ |
|||
private readonly IStore<string> store; |
|||
private IPersistence<State> persistence; |
|||
private State state = new State(); |
|||
|
|||
[CollectionName("Index_Tags")] |
|||
public sealed class State |
|||
{ |
|||
public Dictionary<string, TagInfo> Tags { get; set; } = new Dictionary<string, TagInfo>(); |
|||
} |
|||
|
|||
public sealed class TagInfo |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public int Count { get; set; } = 1; |
|||
} |
|||
|
|||
public TagGrain(IStore<string> store) |
|||
{ |
|||
Guard.NotNull(store, nameof(store)); |
|||
|
|||
this.store = store; |
|||
} |
|||
|
|||
public override Task OnActivateAsync(string key) |
|||
{ |
|||
persistence = store.WithSnapshots<TagGrain, State, string>(key, s => |
|||
{ |
|||
state = s; |
|||
}); |
|||
|
|||
return persistence.ReadAsync(); |
|||
} |
|||
|
|||
public async Task<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids) |
|||
{ |
|||
var result = new HashSet<string>(); |
|||
|
|||
if (names != null) |
|||
{ |
|||
foreach (var tag in names) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(tag)) |
|||
{ |
|||
var tagName = tag.ToLowerInvariant(); |
|||
var tagId = string.Empty; |
|||
|
|||
var found = state.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase)); |
|||
|
|||
if (found.Value != null) |
|||
{ |
|||
tagId = found.Key; |
|||
|
|||
if (ids == null || !ids.Contains(tagId)) |
|||
{ |
|||
found.Value.Count++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
tagId = Guid.NewGuid().ToString(); |
|||
|
|||
state.Tags.Add(tagId, new TagInfo { Name = tagName }); |
|||
} |
|||
|
|||
result.Add(tagId); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (ids != null) |
|||
{ |
|||
foreach (var id in ids) |
|||
{ |
|||
if (!result.Contains(id)) |
|||
{ |
|||
if (state.Tags.TryGetValue(id, out var tagInfo)) |
|||
{ |
|||
tagInfo.Count--; |
|||
|
|||
if (tagInfo.Count <= 0) |
|||
{ |
|||
state.Tags.Remove(id); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
await persistence.WriteSnapshotAsync(state); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public Task<HashSet<string>> GetTagIdsAsync(HashSet<string> names) |
|||
{ |
|||
var result = new HashSet<string>(); |
|||
|
|||
foreach (var name in names) |
|||
{ |
|||
var id = state.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(id)) |
|||
{ |
|||
result.Add(id); |
|||
} |
|||
} |
|||
|
|||
return Task.FromResult(result); |
|||
} |
|||
|
|||
public Task<Dictionary<string, string>> DenormalizeTagsAsync(HashSet<string> ids) |
|||
{ |
|||
var result = new Dictionary<string, string>(); |
|||
|
|||
foreach (var id in ids) |
|||
{ |
|||
if (state.Tags.TryGetValue(id, out var tagInfo)) |
|||
{ |
|||
result[id] = tagInfo.Name; |
|||
} |
|||
} |
|||
|
|||
return Task.FromResult(result); |
|||
} |
|||
|
|||
public Task<Dictionary<string, int>> GetTagsAsync() |
|||
{ |
|||
return Task.FromResult(state.Tags.Values.ToDictionary(x => x.Name, x => x.Count)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Tags |
|||
{ |
|||
public static class TagGroups |
|||
{ |
|||
public const string Assets = "Assets"; |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Assets |
|||
{ |
|||
[EventType(nameof(AssetTagged))] |
|||
public sealed class AssetTagged : AssetEvent |
|||
{ |
|||
public HashSet<string> Tags { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.MongoDb.OData |
|||
{ |
|||
public delegate object ConvertValue(string field, object value); |
|||
|
|||
public static class ValueConversion |
|||
{ |
|||
public static object Convert(string field, object value, ConvertValue converter = null) |
|||
{ |
|||
if (converter == null) |
|||
{ |
|||
return value; |
|||
} |
|||
|
|||
return converter(field, value); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Infrastructure |
|||
{ |
|||
public static class HashSet |
|||
{ |
|||
public static HashSet<T> Of<T>(params T[] items) |
|||
{ |
|||
return new HashSet<T>(items); |
|||
} |
|||
} |
|||
} |
|||
@ -1,2 +1,26 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
@import '_mixins'; |
|||
|
|||
.section { |
|||
border-top: 1px solid $color-border; |
|||
padding: 1rem; |
|||
} |
|||
|
|||
.tag { |
|||
& { |
|||
padding: .25rem 0; |
|||
} |
|||
|
|||
&.active { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
&.active, |
|||
&:hover { |
|||
background: $color-background; |
|||
} |
|||
} |
|||
|
|||
a.tag { |
|||
cursor: pointer !important; |
|||
} |
|||
@ -1,12 +1,15 @@ |
|||
<input type="text" class="form-control" [attr.name]="inputName" (keydown)="onKeyDown($event)" (blur)="markTouched()" |
|||
<div class="form-control {{class}}" (click)="input.focus()" [class.focus]="hasFocus" [class.disabled]="addInput.disabled"> |
|||
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled"> |
|||
{{item}} <i class="icon-close" (click)="remove(i)"></i> |
|||
</span> |
|||
|
|||
<input type="text" class="blank" [attr.name]="inputName" (keydown)="onKeyDown($event)" [class.hidden]="addInput.disabled" #input |
|||
(focus)="focus()" |
|||
(blur)="markTouched()" |
|||
(input)="adjustSize()" |
|||
[formControl]="addInput" |
|||
autocomplete="off" |
|||
autocorrect="off" |
|||
autocapitalize="off" |
|||
placeholder="Press enter to add new item"> |
|||
|
|||
<div class="items"> |
|||
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled"> |
|||
{{item}} <i class="icon-close" (click)="remove(i)"></i> |
|||
</span> |
|||
</div> |
|||
placeholder="+Tag"> |
|||
</div> |
|||
@ -0,0 +1,117 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Tags; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets |
|||
{ |
|||
public class AssetQueryServiceTests |
|||
{ |
|||
private readonly ITagService tagService = A.Fake<ITagService>(); |
|||
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); |
|||
private readonly IAppEntity app = A.Fake<IAppEntity>(); |
|||
private readonly Guid appId = Guid.NewGuid(); |
|||
private readonly string appName = "my-app"; |
|||
private readonly ClaimsPrincipal user; |
|||
private readonly ClaimsIdentity identity = new ClaimsIdentity(); |
|||
private readonly QueryContext context; |
|||
private readonly AssetQueryService sut; |
|||
|
|||
public AssetQueryServiceTests() |
|||
{ |
|||
user = new ClaimsPrincipal(identity); |
|||
|
|||
A.CallTo(() => app.Id).Returns(appId); |
|||
A.CallTo(() => app.Name).Returns(appName); |
|||
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.English); |
|||
|
|||
context = QueryContext.Create(app, user); |
|||
|
|||
A.CallTo(() => tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.IsSameSequenceAs("id1", "id2", "id3"))) |
|||
.Returns(new Dictionary<string, string> |
|||
{ |
|||
["id1"] = "name1", |
|||
["id2"] = "name2", |
|||
["id3"] = "name3" |
|||
}); |
|||
|
|||
sut = new AssetQueryService(tagService, assetRepository); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_find_asset_by_id_and_resolve_tags() |
|||
{ |
|||
var id = Guid.NewGuid(); |
|||
|
|||
A.CallTo(() => assetRepository.FindAssetAsync(id)) |
|||
.Returns(CreateAsset(id, "id1", "id2", "id3")); |
|||
|
|||
var result = await sut.FindAssetAsync(context, id); |
|||
|
|||
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_load_assets_from_ids_and_resolve_tags() |
|||
{ |
|||
var id1 = Guid.NewGuid(); |
|||
var id2 = Guid.NewGuid(); |
|||
|
|||
var ids = HashSet.Of(id1, id2); |
|||
|
|||
A.CallTo(() => assetRepository.QueryAsync(appId, A<HashSet<Guid>>.That.IsSameSequenceAs(ids))) |
|||
.Returns(ResultList.Create(8, |
|||
CreateAsset(id1, "id1", "id2", "id3"), |
|||
CreateAsset(id2))); |
|||
|
|||
var result = await sut.QueryAsync(context, Query.Empty.WithIds(ids)); |
|||
|
|||
Assert.Equal(8, result.Total); |
|||
Assert.Equal(2, result.Count); |
|||
|
|||
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result[0].Tags); |
|||
Assert.Empty(result[1].Tags); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_load_assets_with_query_and_resolve_tags() |
|||
{ |
|||
A.CallTo(() => assetRepository.QueryAsync(appId, "my-query")) |
|||
.Returns(ResultList.Create(8, |
|||
CreateAsset(Guid.NewGuid(), "id1", "id2"), |
|||
CreateAsset(Guid.NewGuid(), "id2", "id3"))); |
|||
|
|||
var result = await sut.QueryAsync(context, Query.Empty.WithODataQuery("my-query")); |
|||
|
|||
Assert.Equal(8, result.Total); |
|||
Assert.Equal(2, result.Count); |
|||
|
|||
Assert.Equal(HashSet.Of("name1", "name2"), result[0].Tags); |
|||
Assert.Equal(HashSet.Of("name2", "name3"), result[1].Tags); |
|||
} |
|||
|
|||
private IAssetEntity CreateAsset(Guid id, params string[] tags) |
|||
{ |
|||
var asset = A.Fake<IAssetEntity>(); |
|||
|
|||
A.CallTo(() => asset.Id).Returns(id); |
|||
A.CallTo(() => asset.Tags).Returns(HashSet.Of(tags)); |
|||
|
|||
return asset; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Orleans; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Tags |
|||
{ |
|||
public class GrainTagServiceTests |
|||
{ |
|||
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); |
|||
private readonly ITagGrain grain = A.Fake<ITagGrain>(); |
|||
private readonly Guid appId = Guid.NewGuid(); |
|||
private readonly GrainTagService sut; |
|||
|
|||
public GrainTagServiceTests() |
|||
{ |
|||
A.CallTo(() => grainFactory.GetGrain<ITagGrain>($"{appId}_Assets", null)) |
|||
.Returns(grain); |
|||
|
|||
sut = new GrainTagService(grainFactory); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_call_grain_when_retrieving_tas() |
|||
{ |
|||
await sut.GetTagsAsync(appId, TagGroups.Assets); |
|||
|
|||
A.CallTo(() => grain.GetTagsAsync()) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_call_grain_when_resolving_tag_ids() |
|||
{ |
|||
var tagNames = new HashSet<string>(); |
|||
|
|||
await sut.GetTagIdsAsync(appId, TagGroups.Assets, tagNames); |
|||
|
|||
A.CallTo(() => grain.GetTagIdsAsync(tagNames)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_call_grain_when_denormalizing_tags() |
|||
{ |
|||
var tagIds = new HashSet<string>(); |
|||
|
|||
await sut.DenormalizeTagsAsync(appId, TagGroups.Assets, tagIds); |
|||
|
|||
A.CallTo(() => grain.DenormalizeTagsAsync(tagIds)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_call_grain_when_normalizing_tags() |
|||
{ |
|||
var tagIds = new HashSet<string>(); |
|||
var tagNames = new HashSet<string>(); |
|||
|
|||
await sut.NormalizeTagsAsync(appId, TagGroups.Assets, tagNames, tagIds); |
|||
|
|||
A.CallTo(() => grain.NormalizeTagsAsync(tagNames, tagIds)) |
|||
.MustHaveHappened(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.States; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Tags |
|||
{ |
|||
public class TagGrainTests |
|||
{ |
|||
private readonly IStore<string> store = A.Fake<IStore<string>>(); |
|||
private readonly IPersistence<TagGrain.State> persistence = A.Fake<IPersistence<TagGrain.State>>(); |
|||
private readonly TagGrain sut; |
|||
|
|||
public TagGrainTests() |
|||
{ |
|||
A.CallTo(() => store.WithSnapshots(A<Type>.Ignored, A<string>.Ignored, A<Func<TagGrain.State, Task>>.Ignored)) |
|||
.Returns(persistence); |
|||
|
|||
sut = new TagGrain(store); |
|||
sut.OnActivateAsync(string.Empty).Wait(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_tags_to_grain() |
|||
{ |
|||
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
|||
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag2", "tag3"), null); |
|||
|
|||
var allTags = await sut.GetTagsAsync(); |
|||
|
|||
Assert.Equal(new Dictionary<string, int> |
|||
{ |
|||
["tag1"] = 1, |
|||
["tag2"] = 2, |
|||
["tag3"] = 1 |
|||
}, allTags); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_tags_if_already_added() |
|||
{ |
|||
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
|||
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2", "tag3"), result1); |
|||
|
|||
var allTags = await sut.GetTagsAsync(); |
|||
|
|||
Assert.Equal(new Dictionary<string, int> |
|||
{ |
|||
["tag1"] = 1, |
|||
["tag2"] = 1, |
|||
["tag3"] = 1 |
|||
}, allTags); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_remove_tags_from_grain() |
|||
{ |
|||
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
|||
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag2", "tag3"), null); |
|||
|
|||
await sut.NormalizeTagsAsync(null, result1); |
|||
|
|||
var allTags = await sut.GetTagsAsync(); |
|||
|
|||
Assert.Equal(new Dictionary<string, int> |
|||
{ |
|||
["tag2"] = 1, |
|||
["tag3"] = 1 |
|||
}, allTags); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_resolve_tag_names() |
|||
{ |
|||
var tagIds = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
|||
var tagNames = await sut.GetTagIdsAsync(HashSet.Of("tag1", "tag2", "invalid1")); |
|||
|
|||
Assert.Equal(tagIds, tagNames); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue