Browse Source

Get contents by ids only.

pull/351/head
Sebastian 7 years ago
parent
commit
e0e9abb26b
  1. 34
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  2. 9
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs
  3. 18
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs
  4. 19
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  5. 92
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  6. 3
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  7. 2
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  8. 41
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  9. 88
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

34
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -25,15 +25,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
internal class MongoContentCollection : MongoRepositoryBase<MongoContentEntity>
{
private readonly IAppProvider appProvider;
private readonly string collectionName;
protected IJsonSerializer Serializer { get; }
public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, string collectionName)
public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider, string collectionName)
: base(database)
{
this.collectionName = collectionName;
this.appProvider = appProvider;
Serializer = serializer;
}
@ -87,6 +90,35 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids, Status[] status = null)
{
var find =
status != null && status.Length > 0 ?
Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id) && x.IsDeleted != true && status.Contains(x.Status)) :
Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id));
var contentItems = await find.Not(x => x.DataText).ToListAsync();
var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList();
var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x)));
var result = new List<(IContentEntity Content, ISchemaEntity Schema)>();
foreach (var entity in contentItems)
{
var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId);
if (schema != null)
{
entity.ParseData(schema.SchemaDef, Serializer);
result.Add((entity, schema));
}
}
return result;
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<Guid> ids, Status[] status = null)
{
var find =

9
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs

@ -29,8 +29,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
internal sealed class MongoContentDraftCollection : MongoContentCollection
{
public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer)
: base(database, serializer, "State_Content_Draft")
public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider)
: base(database, serializer, appProvider, "State_Content_Draft")
{
}
@ -39,6 +39,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await collection.Indexes.CreateManyAsync(
new[]
{
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.Id)
.Ascending(x => x.IsDeleted)),
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedSchemaId)

18
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs

@ -20,8 +20,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
internal sealed class MongoContentPublishedCollection : MongoContentCollection
{
public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer)
: base(database, serializer, "State_Content_Published")
public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider)
: base(database, serializer, appProvider, "State_Content_Published")
{
}
@ -30,8 +30,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await collection.Indexes.CreateManyAsync(
new[]
{
new CreateIndexModel<MongoContentEntity>(Index.Text(x => x.DataText).Ascending(x => x.IndexedSchemaId)),
new CreateIndexModel<MongoContentEntity>(Index.Ascending(x => x.IndexedSchemaId).Ascending(x => x.Id))
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(
Index
.Text(x => x.DataText)
.Ascending(x => x.IndexedSchemaId))
}, ct);
await base.SetupCollectionAsync(collection, ct);

19
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -40,8 +40,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
this.serializer = serializer;
contentsDraft = new MongoContentDraftCollection(database, serializer);
contentsPublished = new MongoContentPublishedCollection(database, serializer);
contentsDraft = new MongoContentDraftCollection(database, serializer, appProvider);
contentsPublished = new MongoContentPublishedCollection(database, serializer, appProvider);
this.database = database;
}
@ -81,6 +81,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids)
{
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema"))
{
if (RequiresPublished(status))
{
return await contentsPublished.QueryAsync(app, ids);
}
else
{
return await contentsDraft.QueryAsync(app, ids, status);
}
}
}
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id)
{
using (Profiler.TraceMethod<MongoContentRepository>())

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

@ -23,6 +23,7 @@ using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var schema = await GetSchemaAsync(context, schemaIdOrName);
CheckPermission(schema, context.User);
CheckPermission(context.User, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
@ -99,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity));
}
return Transform(context, schema, true, content);
return Transform(context, schema, content);
}
}
@ -109,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var schema = await GetSchemaAsync(context, schemaIdOrName);
CheckPermission(schema, context.User);
CheckPermission(context.User, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
@ -120,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (query.Ids?.Count > 0)
{
contents = await contentRepository.QueryAsync(context.App, schema, status, new HashSet<Guid>(query.Ids));
contents = Sort(contents, query.Ids);
contents = SortSet(contents, query.Ids);
}
else
{
@ -129,34 +130,67 @@ namespace Squidex.Domain.Apps.Entities.Contents
contents = await contentRepository.QueryAsync(context.App, schema, status, parsedQuery);
}
return Transform(context, schema, true, contents);
return Transform(context, schema, contents);
}
}
private IContentEntity Transform(QueryContext context, ISchemaEntity schema, bool checkType, IContentEntity content)
public async Task<IList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids)
{
return TransformCore(context, schema, checkType, Enumerable.Repeat(content, 1)).FirstOrDefault();
Guard.NotNull(context, nameof(context));
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetQueryStatus(context);
List<IContentEntity> result;
if (ids?.Count > 0)
{
var contents = await contentRepository.QueryAsync(context.App, status, new HashSet<Guid>(ids));
var permissions = context.User.Permissions();
contents = contents.Where(x => HasPermission(permissions, x.Schema)).ToList();
result = contents.Select(x => Transform(context, x.Schema, x.Content)).ToList();
result = SortList(result, ids).ToList();
}
else
{
result = new List<IContentEntity>();
}
return result;
}
}
private IResultList<IContentEntity> Transform(QueryContext context, ISchemaEntity schema, bool checkType, IResultList<IContentEntity> contents)
private IResultList<IContentEntity> Transform(QueryContext context, ISchemaEntity schema, IResultList<IContentEntity> contents)
{
var transformed = TransformCore(context, schema, checkType, contents);
var transformed = TransformCore(context, schema, contents);
return ResultList.Create(contents.Total, transformed);
}
private static IResultList<IContentEntity> Sort(IResultList<IContentEntity> contents, IReadOnlyList<Guid> ids)
private IContentEntity Transform(QueryContext context, ISchemaEntity schema, IContentEntity content)
{
var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
return TransformCore(context, schema, Enumerable.Repeat(content, 1)).FirstOrDefault();
}
private static IResultList<IContentEntity> SortSet(IResultList<IContentEntity> contents, IReadOnlyList<Guid> ids)
{
return ResultList.Create(contents.Total, SortList(contents, ids));
}
return ResultList.Create(contents.Total, sorted);
private static IEnumerable<IContentEntity> SortList(IEnumerable<IContentEntity> contents, IReadOnlyList<Guid> ids)
{
return ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
}
private IEnumerable<IContentEntity> TransformCore(QueryContext context, ISchemaEntity schema, bool checkType, IEnumerable<IContentEntity> contents)
private IEnumerable<IContentEntity> TransformCore(QueryContext context, ISchemaEntity schema, IEnumerable<IContentEntity> contents)
{
using (Profiler.TraceMethod<ContentQueryService>())
{
var converters = GenerateConverters(context, checkType).ToArray();
var converters = GenerateConverters(context).ToArray();
var scriptText = schema.SchemaDef.Scripts.Query;
@ -170,7 +204,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
if (!context.IsFrontendClient && isScripting)
{
result.Data = scriptEngine.Transform(new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }, scriptText);
var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id };
result.Data = scriptEngine.Transform(ctx, scriptText);
}
result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters);
@ -186,7 +222,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private IEnumerable<FieldConverter> GenerateConverters(QueryContext context, bool checkType)
private IEnumerable<FieldConverter> GenerateConverters(QueryContext context)
{
if (!context.IsFrontendClient)
{
@ -194,11 +230,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden());
}
if (checkType)
{
yield return FieldConverters.ExcludeChangedTypes();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes());
}
yield return FieldConverters.ExcludeChangedTypes();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes());
yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig);
yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig);
@ -274,17 +307,26 @@ namespace Squidex.Domain.Apps.Entities.Contents
return schema;
}
private static void CheckPermission(ISchemaEntity schema, ClaimsPrincipal user)
private static void CheckPermission(ClaimsPrincipal user, params ISchemaEntity[] schemas)
{
var permissions = user.Permissions();
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name);
if (!permissions.Allows(permission))
foreach (var schema in schemas)
{
throw new DomainForbiddenException("You do not have permission for this schema.");
if (!HasPermission(permissions, schema))
{
throw new DomainForbiddenException("You do not have permission for this schema.");
}
}
}
private static bool HasPermission(PermissionSet permissions, ISchemaEntity schema)
{
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name);
return permissions.Allows(permission);
}
private static Status[] GetFindStatus(QueryContext context)
{
if (context.IsFrontendClient)

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

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

2
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{
public interface IContentRepository
{
Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query);

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

@ -106,6 +106,45 @@ namespace Squidex.Areas.Api.Controllers.Contents
}
}
/// <summary>
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="archived">Indicates whether to query content items from the archive.</param>
/// <returns>
/// 200 => Contents retrieved.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[HttpGet]
[Route("content/{app}/")]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids, [FromQuery] bool archived = false)
{
var context = Context().WithArchived(archived);
var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids);
var response = new ContentsDto
{
Total = result.Count,
Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray()
};
if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys();
}
Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag();
return Ok(response);
}
/// <summary>
/// Queries contents.
/// </summary>
@ -124,7 +163,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[Route("content/{app}/{name}/")]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null)
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] bool archived = false)
{
var context = Context().WithArchived(archived);

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

@ -358,7 +358,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(ManyIdDataApi))]
public async Task Should_query_contents_by_id_from_repository_and_transform(bool archive, bool unpublished, params Status[] status)
public async Task Should_query_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status)
{
const int count = 5, total = 200;
@ -380,6 +380,86 @@ namespace Squidex.Domain.Apps.Entities.Contents
.MustHaveHappened(count, Times.Exactly);
}
[Theory]
[MemberData(nameof(ManyIdDataFrontend))]
public async Task Should_query_all_contents_by_id_for_frontend_and_transform(bool archive, bool unpublished, params Status[] status)
{
const int count = 5;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims(true);
SetupSchema();
SetupScripting(ids.ToArray());
SetupContents(status, ids);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var result = await sut.QueryAsync(ctx, ids);
Assert.Equal(ids, result.Select(x => x.Id).ToList());
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Theory]
[MemberData(nameof(ManyIdDataApi))]
public async Task Should_query_all_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status)
{
const int count = 5;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims();
SetupSchema();
SetupScripting(ids.ToArray());
SetupContents(status, ids);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var result = await sut.QueryAsync(ctx, ids);
Assert.Equal(ids, result.Select(x => x.Id).ToList());
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustHaveHappened(count, Times.Exactly);
}
[Fact]
public async Task Should_skip_contents_when_user_has_no_permission()
{
var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList();
SetupClaims(false, false);
SetupSchema();
SetupContents(new Status[0], ids);
var ctx = context;
var result = await sut.QueryAsync(ctx, ids);
Assert.Empty(result);
}
[Fact]
public async Task Should_not_call_repository_if_no_id_defined()
{
var ids = new List<Guid>();
SetupClaims(false, false);
SetupSchema();
var ctx = context;
var result = await sut.QueryAsync(ctx, ids);
Assert.Empty(result);
A.CallTo(() => contentRepository.QueryAsync(app, A<Status[]>.Ignored, A<HashSet<Guid>>.Ignored))
.MustNotHaveHappened();
}
private void SetupClaims(bool isFrontend = false, bool allowSchema = true)
{
if (isFrontend)
@ -414,6 +494,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
.Returns(ResultList.Create(total, ids.Select(x => CreateContent(x)).Shuffle()));
}
private void SetupContents(Status[] status, List<Guid> ids)
{
A.CallTo(() => contentRepository.QueryAsync(app, A<Status[]>.That.IsSameSequenceAs(status), A<HashSet<Guid>>.Ignored))
.Returns(ids.Select(x => (CreateContent(x), schema)).ToList());
}
private void SetupSchema()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name))

Loading…
Cancel
Save