Browse Source

Query services improved.

pull/308/head
Sebastian 8 years ago
parent
commit
7a6a188e4d
  1. 101
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
  3. 20
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  4. 5
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  5. 46
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryContext.cs
  6. 54
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  7. 11
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  8. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  9. 9
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  10. 23
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  11. 54
      src/Squidex.Domain.Apps.Entities/Query.cs
  12. 55
      src/Squidex.Domain.Apps.Entities/QueryContext.cs
  13. 36
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  14. 35
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  15. 1
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  16. 3
      src/Squidex/app/framework/angular/forms/tag-editor.component.scss
  17. 2
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  18. 2
      src/Squidex/app/shared/components/asset.component.html
  19. 41
      src/Squidex/app/shared/components/asset.component.ts
  20. 12
      src/Squidex/app/shared/services/assets.service.spec.ts
  21. 9
      src/Squidex/app/shared/services/assets.service.ts
  22. 10
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  23. 31
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  24. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

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

@ -0,0 +1,101 @@
// ==========================================================================
// 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(sorted, assets.Total);
}
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets)
{
var tags = assets.SelectMany(x => x.Tags).Distinct().ToArray();
var tagsById = await tagService.DenormalizeTagsAsync(appId, "Assets", tags);
foreach (var asset in assets)
{
if (asset.Tags?.Length > 0)
{
var tagNames = new List<string>();
foreach (var id in asset.Tags)
{
if (tagsById.TryGetValue(id, out var name))
{
tagNames.Add(name);
}
}
asset.Tags = tagNames.ToArray();
}
}
}
}
}

2
src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs

@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
string MimeType { get; }
string[] Tags { get; }
string[] Tags { get; set; }
long FileVersion { get; }
}

20
src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

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

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

@ -73,6 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
TotalSize += @event.FileSize;
}
protected void On(AssetTagged @event)
{
Tags = @event.Tags;
}
protected void On(AssetRenamed @event)
{
FileName = @event.FileName;

46
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryContext.cs

@ -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());
}
}
}

54
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -55,12 +55,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.scriptEngine = scriptEngine;
}
public Task ThrowIfSchemaNotExistsAsync(QueryContext context)
public Task ThrowIfSchemaNotExistsAsync(ContentQueryContext context)
{
return GetSchemaAsync(context);
}
public async Task<IContentEntity> FindContentAsync(QueryContext context, Guid id, long version = -1)
public async Task<IContentEntity> FindContentAsync(ContentQueryContext context, Guid id, long version = -1)
{
Guard.NotNull(context, nameof(context));
@ -70,53 +70,47 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var isVersioned = version > EtagVersion.Empty;
var parsedStatus = context.IsFrontendClient ? StatusAll : StatusPublished;
var parsedStatus = context.Base.IsFrontendClient ? StatusAll : StatusPublished;
var content =
isVersioned ?
await FindContentByVersionAsync(id, version) :
await FindContentAsync(context, id, parsedStatus, schema);
await FindContentAsync(context.Base, id, parsedStatus, schema);
if (content == null || (content.Status != Status.Published && !context.IsFrontendClient) || content.SchemaId.Id != schema.Id)
if (content == null || (content.Status != Status.Published && !context.Base.IsFrontendClient) || content.SchemaId.Id != schema.Id)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity));
}
return Transform(context, schema, true, content);
return Transform(context.Base, schema, true, content);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string query)
public async Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Query query)
{
Guard.NotNull(context, nameof(context));
var schema = await GetSchemaAsync(context);
using (Profiler.TraceMethod<ContentQueryService>("QueryAsyncByQuery"))
using (Profiler.TraceMethod<ContentQueryService>())
{
var parsedQuery = ParseQuery(context, query, schema);
var parsedStatus = ParseStatus(context);
var contents = await contentRepository.QueryAsync(context.App, schema, parsedStatus, parsedQuery);
return Transform(context, schema, true, contents);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, IList<Guid> ids)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(ids, nameof(ids));
var parsedStatus = ParseStatus(context.Base);
var schema = await GetSchemaAsync(context);
IResultList<IContentEntity> contents;
using (Profiler.TraceMethod<ContentQueryService>("QueryAsyncByIds"))
{
var parsedStatus = ParseStatus(context);
if (query.Ids?.Count > 0)
{
contents = await contentRepository.QueryAsync(context.Base.App, schema, parsedStatus, new HashSet<Guid>(query.Ids));
contents = Sort(contents, query.Ids);
}
else
{
var parsedQuery = ParseQuery(context.Base, query.ODataQuery, schema);
var contents = await contentRepository.QueryAsync(context.App, schema, parsedStatus, new HashSet<Guid>(ids));
contents = await contentRepository.QueryAsync(context.Base.App, schema, parsedStatus, parsedQuery);
}
return Sort(Transform(context, schema, false, contents), ids);
return Transform(context.Base, schema, true, contents);
}
}
@ -218,18 +212,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public async Task<ISchemaEntity> GetSchemaAsync(QueryContext context)
public async Task<ISchemaEntity> GetSchemaAsync(ContentQueryContext context)
{
ISchemaEntity schema = null;
if (Guid.TryParse(context.SchemaIdOrName, out var id))
{
schema = await appProvider.GetSchemaAsync(context.App.Id, id);
schema = await appProvider.GetSchemaAsync(context.Base.App.Id, id);
}
if (schema == null)
{
schema = await appProvider.GetSchemaAsync(context.App.Id, context.SchemaIdOrName);
schema = await appProvider.GetSchemaAsync(context.Base.App.Id, context.SchemaIdOrName);
}
if (schema == null)

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

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
@ -20,23 +21,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository;
private readonly IAssetQueryService assetQuery;
private readonly IAppProvider appProvider;
public CachingGraphQLService(IMemoryCache cache,
IAppProvider appProvider,
IAssetRepository assetRepository,
IAssetQueryService assetQuery,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(cache)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider;
this.assetRepository = assetRepository;
this.assetQuery = assetQuery;
this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator;
}
@ -53,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var modelContext = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, assetRepository, contentQuery, urlGenerator);
var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator);
return await modelContext.ExecuteAsync(ctx, query);
}

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

@ -10,8 +10,6 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class GraphQLExecutionContext : QueryExecutionContext
@ -19,10 +17,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public IGraphQLUrlGenerator UrlGenerator { get; }
public GraphQLExecutionContext(QueryContext context,
IAssetRepository assetRepository,
IAssetQueryService assetQueryService,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(context, assetRepository, contentQuery)
: base(context, assetQueryService, contentQuery)
{
UrlGenerator = urlGenerator;
}

9
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@ -14,12 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentQueryService
{
Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, IList<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Query query);
Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string query);
Task<IContentEntity> FindContentAsync(ContentQueryContext context, Guid id, long version = EtagVersion.Any);
Task<IContentEntity> FindContentAsync(QueryContext context, Guid id, long version = EtagVersion.Any);
Task ThrowIfSchemaNotExistsAsync(QueryContext context);
Task ThrowIfSchemaNotExistsAsync(ContentQueryContext context);
}
}

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

@ -11,7 +11,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
@ -21,18 +20,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>();
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>();
private readonly IContentQueryService contentQuery;
private readonly IAssetRepository assetRepository;
private readonly IAssetQueryService assetQuery;
private readonly QueryContext context;
public QueryExecutionContext(QueryContext context,
IAssetRepository assetRepository,
IContentQueryService contentQuery)
public QueryExecutionContext(QueryContext context, IAssetQueryService assetQuery, IContentQueryService contentQuery)
{
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(context, nameof(context));
this.assetRepository = assetRepository;
this.assetQuery = assetQuery;
this.contentQuery = contentQuery;
this.context = context;
}
@ -43,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (asset == null)
{
asset = await assetRepository.FindAssetAsync(id);
asset = await assetQuery.FindAssetAsync(context, id);
if (asset != null)
{
@ -60,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (content == null)
{
content = await contentQuery.FindContentAsync(context.WithSchemaId(schemaId), id);
content = await contentQuery.FindContentAsync(new ContentQueryContext(context).WithSchemaId(schemaId), id);
if (content != null)
{
@ -73,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
{
var assets = await assetRepository.QueryAsync(context.App.Id, query);
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithODataQuery(query));
foreach (var asset in assets)
{
@ -85,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
{
var result = await contentQuery.QueryAsync(context.WithSchemaName(schemaIdOrName), query);
var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaName(schemaIdOrName), Query.Empty.WithODataQuery(query));
foreach (var content in result)
{
@ -103,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (notLoadedAssets.Count > 0)
{
var assets = await assetRepository.QueryAsync(context.App.Id, notLoadedAssets);
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithIds(notLoadedAssets));
foreach (var asset in assets)
{
@ -122,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (notLoadedContents.Count > 0)
{
var result = await contentQuery.QueryAsync(context.WithSchemaId(schemaId), notLoadedContents);
var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaId(schemaId), Query.Empty.WithIds(notLoadedContents));
foreach (var content in result)
{

54
src/Squidex.Domain.Apps.Entities/Query.cs

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

55
src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs → src/Squidex.Domain.Apps.Entities/QueryContext.cs

@ -5,14 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities.Contents
namespace Squidex.Domain.Apps.Entities
{
public sealed class QueryContext : Cloneable<QueryContext>
{
@ -20,38 +19,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
public IAppEntity App { get; private set; }
public IEnumerable<Language> Languages { get; private set; }
public string SchemaIdOrName { get; private set; }
public bool Archived { get; private set; }
public bool Flatten { get; private set; }
public IEnumerable<Language> Languages { get; private set; }
private QueryContext()
{
}
public static QueryContext Create(IAppEntity app, ClaimsPrincipal user, IEnumerable<string> languageCodes = null)
public static QueryContext Create(IAppEntity app, ClaimsPrincipal user)
{
var result = new QueryContext { App = app, User = user };
if (languageCodes != null)
{
var languages = new List<Language>();
foreach (var iso2Code in languageCodes)
{
if (Language.TryGetLanguage(iso2Code, out var language))
{
languages.Add(language);
}
}
result.Languages = languages;
}
return result;
return new QueryContext { App = app, User = user };
}
public QueryContext WithArchived(bool archived)
@ -64,14 +44,27 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Clone(c => c.Flatten = flatten);
}
public QueryContext WithSchemaName(string name)
public QueryContext WithLanguages(IEnumerable<string> languageCodes)
{
return Clone(c => c.SchemaIdOrName = name);
}
if (languageCodes != null)
{
return Clone(c =>
{
var languages = new List<Language>();
public QueryContext WithSchemaId(Guid id)
{
return Clone(c => c.SchemaIdOrName = id.ToString());
foreach (var iso2Code in languageCodes)
{
if (Language.TryGetLanguage(iso2Code, out var language))
{
languages.Add(language);
}
}
c.Languages = languages;
});
}
return this;
}
public bool IsFrontendClient

36
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
@ -34,21 +35,21 @@ namespace Squidex.Areas.Api.Controllers.Assets
[SwaggerTag(nameof(Assets))]
public sealed class AssetsController : ApiController
{
private readonly IAssetRepository assetRepository;
private readonly IAssetQueryService assetQuery;
private readonly IAssetStatsRepository assetStatsRepository;
private readonly IAppPlansProvider appPlanProvider;
private readonly AssetConfig assetsConfig;
public AssetsController(
ICommandBus commandBus,
IAssetRepository assetRepository,
IAssetQueryService assetQuery,
IAssetStatsRepository assetStatsRepository,
IAppPlansProvider appPlanProvider,
IOptions<AssetConfig> assetsConfig)
: base(commandBus)
{
this.assetsConfig = assetsConfig.Value;
this.assetRepository = assetRepository;
this.assetQuery = assetQuery;
this.assetStatsRepository = assetStatsRepository;
this.appPlanProvider = appPlanProvider;
}
@ -72,25 +73,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)]
public async Task<IActionResult> GetAssets(string app, [FromQuery] string ids = null)
{
HashSet<Guid> idsList = null;
var context = Context();
if (!string.IsNullOrWhiteSpace(ids))
{
idsList = new HashSet<Guid>();
foreach (var id in ids.Split(','))
{
if (Guid.TryParse(id, out var guid))
{
idsList.Add(guid);
}
}
}
var assets =
idsList?.Count > 0 ?
await assetRepository.QueryAsync(App.Id, idsList) :
await assetRepository.QueryAsync(App.Id, Request.QueryString.ToString());
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids));
var response = AssetsDto.FromAssets(assets);
@ -115,7 +100,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)]
public async Task<IActionResult> GetAsset(string app, Guid id)
{
var entity = await assetRepository.FindAssetAsync(id);
var context = Context();
var entity = await assetQuery.FindAssetAsync(context, id);
if (entity == null)
{
@ -270,5 +257,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
return assetFile;
}
private QueryContext Context()
{
return QueryContext.Create(App, User);
}
}
}

35
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -16,6 +16,7 @@ using NodaTime.Text;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
@ -65,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQL(string app, [FromBody] GraphQLQuery query)
{
var result = await graphQl.QueryAsync(Context(), query);
var result = await graphQl.QueryAsync(Context().Base, query);
if (result.Errors?.Length > 0)
{
@ -97,32 +98,14 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(2)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null)
{
List<Guid> idsList = null;
var context = Context().WithArchived(archived).WithSchemaName(name);
if (!string.IsNullOrWhiteSpace(ids))
{
idsList = new List<Guid>();
foreach (var id in ids.Split(','))
{
if (Guid.TryParse(id, out var guid))
{
idsList.Add(guid);
}
}
}
var context = Context().WithSchemaName(name).WithArchived(archived);
var result =
idsList?.Count > 0 ?
await contentQuery.QueryAsync(context, idsList) :
await contentQuery.QueryAsync(context, Request.QueryString.ToString());
var result = await contentQuery.QueryAsync(context, Query.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var response = new ContentsDto
{
Total = result.Total,
Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray()
Items = result.Take(200).Select(x => ContentDto.FromContent(x, context.Base)).ToArray()
};
var options = controllerOptions.Value;
@ -157,7 +140,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context().WithSchemaName(name);
var content = await contentQuery.FindContentAsync(context, id);
var response = ContentDto.FromContent(content, context);
var response = ContentDto.FromContent(content, context.Base);
Response.Headers["ETag"] = content.Version.ToString();
@ -193,7 +176,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context().WithSchemaName(name);
var content = await contentQuery.FindContentAsync(context, id, version);
var response = ContentDto.FromContent(content, context);
var response = ContentDto.FromContent(content, context.Base);
Response.Headers["ETag"] = content.Version.ToString();
@ -498,9 +481,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
return new ChangeContentStatus { Status = status, ContentId = id, DueTime = dt };
}
private QueryContext Context()
private ContentQueryContext Context()
{
return QueryContext.Create(App, User, Request.Headers["X-Languages"]).WithFlatten(Request.Headers.ContainsKey("X-Flatten"));
return new ContentQueryContext(QueryContext.Create(App, User).WithLanguages(Request.Headers["X-Languages"])).WithFlatten(Request.Headers.ContainsKey("X-Flatten"));
}
}
}

1
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -10,6 +10,7 @@ using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;

3
src/Squidex/app/framework/angular/forms/tag-editor.component.scss

@ -18,6 +18,7 @@
border: 0;
background: transparent;
min-width: 40px;
max-width: 100%;
}
&:focus,
@ -38,6 +39,7 @@
.item {
& {
@include border-radius(10px);
@include truncate;
display: inline-block;
color: $color-dark-foreground;
cursor: default;
@ -48,6 +50,7 @@
font-size: .8rem;
font-weight: normal;
line-height: 20px;
vertical-align: middle;
}
&-container {

2
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -176,7 +176,7 @@ export class TagEditorComponent implements ControlValueAccessor {
const value = <string>this.addInput.value;
if (!value || value.length === 0) {
this.updateItems(this.items.slice(0, this.items.length - 2));
this.updateItems(this.items.slice(0, this.items.length - 1));
return false;
}

2
src/Squidex/app/shared/components/asset.component.html

@ -54,7 +54,7 @@
</div>
</div>
<div>
<sqx-tag-editor class="blank"></sqx-tag-editor>
<sqx-tag-editor [formControl]="tagInput" class="blank"></sqx-tag-editor>
</div>
<div class="file-info">
<ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}

41
src/Squidex/app/shared/components/asset.component.ts

@ -5,8 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import {
AppsState,
@ -21,6 +23,7 @@ import {
Types,
Versioned
} from '@app/shared/internal';
import { TagAssetDto } from '@appshared/services/assets.service';
@Component({
selector: 'sqx-asset',
@ -30,7 +33,9 @@ import {
fadeAnimation
]
})
export class AssetComponent implements OnInit {
export class AssetComponent implements OnDestroy, OnInit {
private tagSubscription: Subscription;
@Input()
public initFile: File;
@ -72,6 +77,8 @@ export class AssetComponent implements OnInit {
public renameForm = new RenameAssetForm(this.formBuilder);
public tagInput = new FormControl();
public progress = 0;
constructor(
@ -102,6 +109,18 @@ export class AssetComponent implements OnInit {
} else {
this.updateAsset(this.asset, false);
}
this.tagSubscription =
this.tagInput.valueChanges.pipe(
distinctUntilChanged(),
debounceTime(2000)
).subscribe(tags => {
this.tagAsset(tags);
});
}
public ngOnDestroy() {
this.tagSubscription.unsubscribe();
}
public updateFile(files: FileList) {
@ -140,6 +159,19 @@ export class AssetComponent implements OnInit {
}
}
public tagAsset(tags: string[]) {
if (tags) {
const requestDto = new TagAssetDto(tags);
this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version)
.subscribe(dto => {
this.updateAsset(this.asset.tag(tags, this.authState.user!.token, dto.version), true);
}, error => {
this.dialogs.notifyError(error);
});
}
}
public renameStart() {
if (!this.isDisabled) {
this.renameForm.load(this.asset);
@ -176,9 +208,10 @@ export class AssetComponent implements OnInit {
private updateAsset(asset: AssetDto, emitEvent: boolean) {
this.asset = asset;
this.progress = 0;
this.tagInput.setValue(asset.tags);
if (emitEvent) {
this.emitUpdated(asset);
}

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

@ -17,10 +17,10 @@ import {
AssetsService,
DateTime,
RenameAssetDto,
TagAssetDto,
Version,
Versioned
} from './../';
import { TagAssetDto } from '@appshared/services/assets.service';
describe('AssetDto', () => {
const creation = DateTime.today();
@ -40,6 +40,16 @@ describe('AssetDto', () => {
expect(asset_2.version).toEqual(newVersion);
});
it('should update tag property and user info when tagged', () => {
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, [], 'url', version);
const asset_2 = asset_1.tag(['tag1', 'tag2'], modifier, newVersion, modified);
expect(asset_2.tags).toEqual(['tag1', 'tag2']);
expect(asset_2.lastModified).toEqual(modified);
expect(asset_2.lastModifiedBy).toEqual(modifier);
expect(asset_2.version).toEqual(newVersion);
});
it('should update file properties when uploading', () => {
const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2);

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

@ -70,6 +70,15 @@ export class AssetDto extends Model {
});
}
public tag(tags: string[], user: string, version: Version, now?: DateTime): AssetDto {
return this.with({
tags,
lastModified: now || DateTime.now(),
lastModifiedBy: user,
version
});
}
public rename(fileName: string, user: string, version: Version, now?: DateTime): AssetDto {
return this.with({
fileName,

10
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly ClaimsPrincipal user;
private readonly ClaimsIdentity identity = new ClaimsIdentity();
private readonly EdmModelBuilder modelBuilder = A.Fake<EdmModelBuilder>();
private readonly QueryContext context;
private readonly ContentQueryContext context;
private readonly ContentQueryService sut;
public ContentQueryServiceTests()
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => schema.SchemaDef).Returns(new Schema("my-schema"));
context = QueryContext.Create(app, user);
context = new ContentQueryContext(QueryContext.Create(app, user));
sut = new ContentQueryService(contentRepository, contentVersionLoader, appProvider, scriptEngine, modelBuilder);
}
@ -187,7 +187,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored))
.Returns(ResultList.Create(Enumerable.Repeat(content, count), total));
var result = await sut.QueryAsync(context.WithSchemaId(schemaId).WithArchived(archive), string.Empty);
var result = await sut.QueryAsync(context.WithSchemaId(schemaId).WithArchived(archive), A<Query>.That.Matches(x => x.ODataQuery == string.Empty));
Assert.Equal(contentData, result[0].Data);
Assert.Equal(content.Id, result[0].Id);
@ -215,7 +215,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => modelBuilder.BuildEdmModel(schema, app))
.Throws(new ODataException());
return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context.WithSchemaId(schemaId), "query"));
return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context.WithSchemaId(schemaId), A<Query>.That.Matches(x => x.ODataQuery == "query")));
}
public static IEnumerable<object[]> ManyIdRequestData = new[]
@ -241,7 +241,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<HashSet<Guid>>.Ignored))
.Returns(ResultList.Create(ids.Select(x => CreateContent(x)).Shuffle(), total));
var result = await sut.QueryAsync(context.WithSchemaId(schemaId).WithArchived(archive), ids);
var result = await sut.QueryAsync(context.WithSchemaId(schemaId).WithArchived(archive), A<Query>.Ignored);
Assert.Equal(ids, result.Select(x => x.Id).ToList());
Assert.Equal(total, result.Total);

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

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var assets = new List<IAssetEntity> { asset };
A.CallTo(() => assetRepository.QueryAsync(app.Id, "?$take=30&$skip=5&$search=my-query"))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query")))
.Returns(ResultList.Create(assets, 0));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -134,7 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var assets = new List<IAssetEntity> { asset };
A.CallTo(() => assetRepository.QueryAsync(app.Id, "?$take=30&$skip=5&$search=my-query"))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query")))
.Returns(ResultList.Create(assets, 10));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -203,7 +203,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}}
}}";
A.CallTo(() => assetRepository.FindAssetAsync(assetId))
A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId))
.Returns(asset);
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -287,7 +287,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contents = new List<IContentEntity> { content };
A.CallTo(() => contentQuery.QueryAsync(ContextMatch(), "?$top=30&$skip=5"))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(contents, 0));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -421,7 +421,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contents = new List<IContentEntity> { content };
A.CallTo(() => contentQuery.QueryAsync(ContextMatch(), "?$top=30&$skip=5"))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(contents, 10));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -539,7 +539,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}}
}}";
A.CallTo(() => contentQuery.FindContentAsync(ContextMatch(), contentId, EtagVersion.Any))
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content);
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -632,10 +632,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var refContents = new List<IContentEntity> { contentRef };
A.CallTo(() => contentQuery.FindContentAsync(ContextMatch(), contentId, EtagVersion.Any))
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content);
A.CallTo(() => contentQuery.QueryAsync(ContextMatch(), A<IList<Guid>>.That.IsSameSequenceAs(new[] { contentRefId })))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.Ignored))
.Returns(ResultList.Create(refContents, 0));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -692,10 +692,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var refAssets = new List<IAssetEntity> { assetRef };
A.CallTo(() => contentQuery.FindContentAsync(ContextMatch(), contentId, EtagVersion.Any))
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content);
A.CallTo(() => assetRepository.QueryAsync(app.Id, A<HashSet<Guid>>.That.Matches(x => x.Contains(assetRefId))))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.Ignored))
.Returns(ResultList.Create(refAssets, 0));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -751,7 +751,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}}
}}";
A.CallTo(() => contentQuery.FindContentAsync(ContextMatch(), contentId, EtagVersion.Any))
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content);
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -764,9 +764,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
AssertResult(expected, result, false);
}
private QueryContext ContextMatch()
private QueryContext MatchsAssetContext()
{
return A<QueryContext>.That.Matches(x => x.App == app && x.SchemaIdOrName == schema.Id.ToString() && x.User == user && !x.Archived);
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user && !x.Archived);
}
private ContentQueryContext MatchsContentContext()
{
return A<ContentQueryContext>.That.Matches(x => x.Base.App == app && x.Base.User == user && !x.Base.Archived && x.SchemaIdOrName == schema.Id.ToString());
}
}
}

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

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
protected static readonly string appName = "my-app";
protected readonly Schema schemaDef;
protected readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
protected readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
protected readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
protected readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
protected readonly IAppProvider appProvider = A.Fake<IAppProvider>();
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas);
sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator());
sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator());
}
protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null)

Loading…
Cancel
Save