diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs index 4afac77cb..b282972f2 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs @@ -94,10 +94,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents { return contentData; } - set - { - contentData = value; - } } public void ParseData(Schema schema) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs index c2b169aef..c2cc3751d 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs @@ -10,12 +10,11 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.OData; +using Microsoft.OData.UriParser; using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.Edm; using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors; using Squidex.Domain.Apps.Read.Schemas; @@ -30,7 +29,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; private readonly ISchemaProvider schemas; - private readonly EdmModelBuilder modelBuilder; protected static FilterDefinitionBuilder Filter { @@ -64,123 +62,89 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents } } - public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas, EdmModelBuilder modelBuilder) + public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas) { Guard.NotNull(database, nameof(database)); - Guard.NotNull(modelBuilder, nameof(modelBuilder)); Guard.NotNull(schemas, nameof(schemas)); this.schemas = schemas; this.database = database; - this.modelBuilder = modelBuilder; } - public async Task> QueryAsync(IAppEntity app, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery) + public async Task> QueryAsync(IAppEntity appEntity, ISchemaEntity schemaEntity, bool nonPublished, HashSet ids, ODataUriParser odataQuery) { - var contentEntities = (List)null; + var collection = GetCollection(appEntity.Id); - await ForSchemaAsync(app.Id, schemaId, async (collection, schemaEntity) => + IFindFluent cursor; + try { - IFindFluent cursor; - try - { - var model = modelBuilder.BuildEdmModel(schemaEntity, app); - - var parser = model.ParseQuery(odataQuery); - - cursor = - collection - .Find(parser, ids, schemaEntity.Id, schemaEntity.Schema, nonPublished) - .Take(parser) - .Skip(parser) - .Sort(parser, schemaEntity.Schema); - } - catch (NotSupportedException) - { - throw new ValidationException("This odata operation is not supported"); - } - catch (NotImplementedException) - { - throw new ValidationException("This odata operation is not supported"); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - - var entities = await cursor.ToListAsync(); - - foreach (var entity in entities) - { - entity.ParseData(schemaEntity.Schema); - } - - contentEntities = entities.OfType().ToList(); - }); - - return contentEntities; + cursor = + collection + .Find(odataQuery, ids, schemaEntity.Id, schemaEntity.Schema, nonPublished) + .Take(odataQuery) + .Skip(odataQuery) + .Sort(odataQuery, schemaEntity.Schema); + } + catch (NotSupportedException) + { + throw new ValidationException("This odata operation is not supported"); + } + catch (NotImplementedException) + { + throw new ValidationException("This odata operation is not supported"); + } + + var entities = await cursor.ToListAsync(); + + foreach (var entity in entities) + { + entity.ParseData(schemaEntity.Schema); + } + + return entities; } - public async Task CountAsync(IAppEntity app, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery) + public Task CountAsync(IAppEntity appEntity, ISchemaEntity schemaEntity, bool nonPublished, HashSet ids, ODataUriParser odataQuery) { - var contentsCount = 0L; + var collection = GetCollection(appEntity.Id); - await ForSchemaAsync(app.Id, schemaId, async (collection, schemaEntity) => + IFindFluent cursor; + try { - IFindFluent cursor; - try - { - var model = modelBuilder.BuildEdmModel(schemaEntity, app); - - var parser = model.ParseQuery(odataQuery); - - cursor = collection.Find(parser, ids, schemaEntity.Id, schemaEntity.Schema, nonPublished); - } - catch (NotSupportedException) - { - throw new ValidationException("This odata operation is not supported"); - } - catch (NotImplementedException) - { - throw new ValidationException("This odata operation is not supported"); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - - contentsCount = await cursor.CountAsync(); - }); - - return contentsCount; + cursor = collection.Find(odataQuery, ids, schemaEntity.Id, schemaEntity.Schema, nonPublished); + } + catch (NotSupportedException) + { + throw new ValidationException("This odata operation is not supported"); + } + catch (NotImplementedException) + { + throw new ValidationException("This odata operation is not supported"); + } + + return cursor.CountAsync(); } public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds) { - var contentEntities = (List)null; + var collection = GetCollection(appId); - await ForAppIdAsync(appId, async collection => - { - contentEntities = - await collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Project(Projection.Include(x => x.Id)) - .ToListAsync(); - }); + var contentEntities = + await collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Project(Projection.Include(x => x.Id)) + .ToListAsync(); return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); } - public async Task FindContentAsync(IAppEntity app, Guid schemaId, Guid id) + public async Task FindContentAsync(IAppEntity appEntity, ISchemaEntity schemaEntity, Guid id) { - var contentEntity = (MongoContentEntity)null; + var collection = GetCollection(appEntity.Id); - await ForSchemaAsync(app.Id, schemaId, async (collection, schemaEntity) => - { - contentEntity = - await collection.Find(x => x.Id == id) - .FirstOrDefaultAsync(); + var contentEntity = + await collection.Find(x => x.Id == id) + .FirstOrDefaultAsync(); - contentEntity?.ParseData(schemaEntity.Schema); - }); + contentEntity?.ParseData(schemaEntity.Schema); return contentEntity; } diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs new file mode 100644 index 000000000..5e06b3ba8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs @@ -0,0 +1,168 @@ +// ========================================================================== +// ContentQueryService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.OData; +using Microsoft.OData.UriParser; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Contents.Edm; +using Squidex.Domain.Apps.Read.Contents.Repositories; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; + +// ReSharper disable InvertIf +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace Squidex.Domain.Apps.Read.Contents +{ + public sealed class ContentQueryService : IContentQueryService + { + private readonly IContentRepository contentRepository; + private readonly ISchemaProvider schemas; + private readonly IScriptEngine scriptEngine; + private readonly EdmModelBuilder modelBuilder; + + public ContentQueryService( + IContentRepository contentRepository, + ISchemaProvider schemas, + IScriptEngine scriptEngine, + EdmModelBuilder modelBuilder) + { + Guard.NotNull(contentRepository, nameof(contentRepository)); + Guard.NotNull(scriptEngine, nameof(scriptEngine)); + Guard.NotNull(modelBuilder, nameof(modelBuilder)); + Guard.NotNull(schemas, nameof(schemas)); + + this.contentRepository = contentRepository; + this.schemas = schemas; + this.scriptEngine = scriptEngine; + this.modelBuilder = modelBuilder; + } + + public async Task<(ISchemaEntity SchemaEntity, IContentEntity ContentEntity)> FindContentAsync(IAppEntity appEntity, string schemaIdOrName, ClaimsPrincipal user, Guid id) + { + Guard.NotNull(appEntity, nameof(appEntity)); + Guard.NotNull(user, nameof(user)); + Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); + + var schemaEntity = await FindSchemaAsync(appEntity, schemaIdOrName); + + var contentEntity = await contentRepository.FindContentAsync(appEntity, schemaEntity, id); + + if (contentEntity == null) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); + } + + contentEntity = TransformContent(user, schemaEntity, new List { contentEntity })[0]; + + return (schemaEntity, contentEntity); + } + + public async Task<(ISchemaEntity SchemaEntity, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity appEntity, string schemaIdOrName, ClaimsPrincipal user, HashSet ids, string query) + { + Guard.NotNull(appEntity, nameof(appEntity)); + Guard.NotNull(user, nameof(user)); + Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); + + var schemaEntity = await FindSchemaAsync(appEntity, schemaIdOrName); + + var parsedQuery = ParseQuery(appEntity, query, schemaEntity); + + var isFrontendClient = user.IsInClient("squidex-frontend"); + + var taskForItems = contentRepository.QueryAsync(appEntity, schemaEntity, isFrontendClient, ids, parsedQuery); + var taskForCount = contentRepository.CountAsync(appEntity, schemaEntity, isFrontendClient, ids, parsedQuery); + + await Task.WhenAll(taskForItems, taskForCount); + + var list = TransformContent(user, schemaEntity, taskForItems.Result.ToList()); + + return (schemaEntity, taskForCount.Result, list); + } + + private List TransformContent(ClaimsPrincipal user, ISchemaEntity schemaEntity, List contentEntities) + { + var scriptText = schemaEntity.ScriptQuery; + + if (!string.IsNullOrWhiteSpace(scriptText)) + { + for (var i = 0; i < contentEntities.Count; i++) + { + var contentEntity = contentEntities[i]; + var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = contentEntity.Data, ContentId = contentEntity.Id }, scriptText); + + contentEntities[i] = SimpleMapper.Map(contentEntity, new Content { Data = contentData }); + } + } + + return contentEntities; + } + + private ODataUriParser ParseQuery(IAppEntity appEntity, string query, ISchemaEntity schemaEntity) + { + try + { + var model = modelBuilder.BuildEdmModel(schemaEntity, appEntity); + + return model.ParseQuery(query); + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } + } + + public async Task FindSchemaAsync(IEntity appEntity, string schemaIdOrName) + { + Guard.NotNull(appEntity, nameof(appEntity)); + + ISchemaEntity schema = null; + + if (Guid.TryParse(schemaIdOrName, out var id)) + { + schema = await schemas.FindSchemaByIdAsync(id); + } + + if (schema == null) + { + schema = await schemas.FindSchemaByNameAsync(appEntity.Id, schemaIdOrName); + } + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); + } + + return schema; + } + + private sealed class Content : IContentEntity + { + public Guid Id { get; set; } + public Guid AppId { get; set; } + public long Version { get; set; } + public bool IsPublished { get; set; } + public Instant Created { get; set; } + public Instant LastModified { get; set; } + public RefToken CreatedBy { get; set; } + public RefToken LastModifiedBy { get; set; } + public NamedContentData Data { get; set; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelExtensions.cs b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelExtensions.cs new file mode 100644 index 000000000..721265b9f --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelExtensions.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// EdmModelExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Squidex.Domain.Apps.Read.Contents.Edm +{ + public static class EdmModelExtensions + { + public static ODataUriParser ParseQuery(this IEdmModel model, string query) + { + query = query ?? string.Empty; + + var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); + + if (query.StartsWith("?")) + { + query = query.Substring(1); + } + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs index 595082711..48304f0fa 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs @@ -11,13 +11,10 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.Schemas.Repositories; -using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; @@ -30,12 +27,10 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService, IEventConsumer { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(60); - private readonly IContentRepository contentRepository; + private readonly IContentQueryService contentQuery; private readonly IGraphQLUrlGenerator urlGenerator; private readonly IAssetRepository assetRepository; private readonly ISchemaRepository schemaRepository; - private readonly ISchemaProvider schemas; - private readonly IScriptEngine scriptEngine; public string Name { @@ -49,26 +44,20 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL public CachingGraphQLService(IMemoryCache cache, IAssetRepository assetRepository, - IContentRepository contentRepository, + IContentQueryService contentQuery, IGraphQLUrlGenerator urlGenerator, - ISchemaRepository schemaRepository, - ISchemaProvider schemas, - IScriptEngine scriptEngine) + ISchemaRepository schemaRepository) : base(cache) { - Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(schemaRepository, nameof(schemaRepository)); Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(urlGenerator, nameof(urlGenerator)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(schemas, nameof(schemas)); + Guard.NotNull(contentQuery, nameof(urlGenerator)); + Guard.NotNull(contentQuery, nameof(contentQuery)); this.assetRepository = assetRepository; - this.contentRepository = contentRepository; + this.contentQuery = contentQuery; this.urlGenerator = urlGenerator; this.schemaRepository = schemaRepository; - this.schemas = schemas; - this.scriptEngine = scriptEngine; } public Task ClearAsync() @@ -92,7 +81,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL Guard.NotNull(query, nameof(query)); var modelContext = await GetModelAsync(app); - var queryContext = new QueryContext(app, assetRepository, contentRepository, urlGenerator, schemas, scriptEngine, user); + var queryContext = new QueryContext(app, assetRepository, contentQuery, urlGenerator, user); return await modelContext.ExecuteAsync(queryContext, query); } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs index 3fa5fabb0..48a2d3000 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs @@ -16,9 +16,9 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL { bool CanGenerateAssetSourceUrl { get; } - string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity); + string GenerateAssetUrl(IAppEntity app, IAssetEntity assetEntity); - string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity); + string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); string GenerateAssetSourceUrl(IAppEntity appEntity, IAssetEntity assetEntity); diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs index 17cfca761..0ed158264 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs @@ -29,12 +29,10 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL { private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); - private readonly IContentRepository contentRepository; + private readonly IContentQueryService contentQuery; private readonly IAssetRepository assetRepository; private readonly IGraphQLUrlGenerator urlGenerator; - private readonly IScriptEngine scriptEngine; - private readonly ISchemaProvider schemas; - private readonly IAppEntity app; + private readonly IAppEntity appEntity; private readonly ClaimsPrincipal user; public IGraphQLUrlGenerator UrlGenerator @@ -43,31 +41,23 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL } public QueryContext( - IAppEntity app, + IAppEntity appEntity, IAssetRepository assetRepository, - IContentRepository contentRepository, + IContentQueryService contentQuery, IGraphQLUrlGenerator urlGenerator, - ISchemaProvider schemas, - IScriptEngine scriptEngine, ClaimsPrincipal user) { - Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(schemas, nameof(schemas)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(urlGenerator, nameof(urlGenerator)); + Guard.NotNull(contentQuery, nameof(contentQuery)); + Guard.NotNull(appEntity, nameof(appEntity)); Guard.NotNull(user, nameof(user)); - Guard.NotNull(app, nameof(app)); - this.contentRepository = contentRepository; this.assetRepository = assetRepository; - this.schemas = schemas; - this.scriptEngine = scriptEngine; + this.appEntity = appEntity; + this.contentQuery = contentQuery; this.urlGenerator = urlGenerator; - this.user = user; - - this.app = app; } public async Task FindAssetAsync(Guid id) @@ -93,18 +83,11 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL if (content == null) { - var schema = await schemas.FindSchemaByIdAsync(schemaId).ConfigureAwait(false); + content = (await contentQuery.FindContentAsync(appEntity, schemaId.ToString(), user, id).ConfigureAwait(false)).ContentEntity; - if (schema != null) + if (content != null) { - content = await contentRepository.FindContentAsync(app, schemaId, id).ConfigureAwait(false); - - if (content != null) - { - content.Data = scriptEngine.Transform(new ScriptContext { Data = content.Data, ContentId = content.Id, User = user }, schema.ScriptQuery); - - cachedContents[content.Id] = content; - } + cachedContents[content.Id] = content; } } @@ -113,7 +96,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL public async Task> QueryAssetsAsync(string query, int skip = 0, int take = 10) { - var assets = await assetRepository.QueryAsync(app.Id, null, null, query, take, skip); + var assets = await assetRepository.QueryAsync(appEntity.Id, null, null, query, take, skip); foreach (var asset in assets) { @@ -125,23 +108,14 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL public async Task> QueryContentsAsync(Guid schemaId, string query) { - var result = new List(); + var contents = (await contentQuery.QueryWithCountAsync(appEntity, schemaId.ToString(), user, null, query).ConfigureAwait(false)).Items; - var schema = await schemas.FindSchemaByIdAsync(schemaId).ConfigureAwait(false); - - if (schema != null) + foreach (var content in contents) { - result.AddRange(await contentRepository.QueryAsync(app, schemaId, false, null, query).ConfigureAwait(false)); - - foreach (var content in result) - { - content.Data = scriptEngine.Transform(new ScriptContext { Data = content.Data, ContentId = content.Id, User = user }, schema.ScriptQuery); - - cachedContents[content.Id] = content; - } + cachedContents[content.Id] = content; } - return result; + return contents; } public Task> GetReferencedAssetsAsync(JToken value) @@ -159,7 +133,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL if (notLoadedAssets.Count > 0) { - var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, null, int.MaxValue).ConfigureAwait(false); + var assets = await assetRepository.QueryAsync(appEntity.Id, null, notLoadedAssets, null, int.MaxValue).ConfigureAwait(false); foreach (var asset in assets) { @@ -185,18 +159,11 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL if (notLoadedContents.Count > 0) { - var schema = await schemas.FindSchemaByIdAsync(schemaId).ConfigureAwait(false); + var contents = (await contentQuery.QueryWithCountAsync(appEntity, schemaId.ToString(), user, notLoadedContents, null).ConfigureAwait(false)).Items; - if (schema != null) + foreach (var content in contents) { - var contents = await contentRepository.QueryAsync(app, schemaId, false, notLoadedContents, null).ConfigureAwait(false); - - foreach (var content in contents) - { - content.Data = scriptEngine.Transform(new ScriptContext { Data = content.Data, ContentId = content.Id, User = user }, schema.ScriptQuery); - - cachedContents[content.Id] = content; - } + cachedContents[content.Id] = content; } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs index 6cd71fe70..64843966e 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs @@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Read.Contents { bool IsPublished { get; } - NamedContentData Data { get; set; } + NamedContentData Data { get;} } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs new file mode 100644 index 000000000..6aeb554af --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// IContentQueryService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Schemas; + +namespace Squidex.Domain.Apps.Read.Contents +{ + public interface IContentQueryService + { + Task<(ISchemaEntity SchemaEntity, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity appEntity, string schemaIdOrName, ClaimsPrincipal user, HashSet ids, string query); + + Task<(ISchemaEntity SchemaEntity, IContentEntity ContentEntity)> FindContentAsync(IAppEntity appEntity, string schemaIdOrName, ClaimsPrincipal user, Guid id); + + Task FindSchemaAsync(IEntity appEntity, string schemaIdOrName); + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs index de71ed551..4915107b1 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs @@ -9,18 +9,20 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.OData.UriParser; using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Schemas; namespace Squidex.Domain.Apps.Read.Contents.Repositories { public interface IContentRepository { - Task> QueryAsync(IAppEntity app, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery); + Task> QueryAsync(IAppEntity appEntity, ISchemaEntity schemaEntity, bool nonPublished, HashSet ids, ODataUriParser odataQuery); Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds); - Task CountAsync(IAppEntity app, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery); + Task CountAsync(IAppEntity appEntity, ISchemaEntity schemaEntity, bool nonPublished, HashSet ids, ODataUriParser odataQuery); - Task FindContentAsync(IAppEntity app, Guid schemaId, Guid id); + Task FindContentAsync(IAppEntity appEntity, ISchemaEntity schemaEntity, Guid id); } } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index c2919fe80..e3e68b312 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -16,6 +16,7 @@ using NSwag.Annotations; using Squidex.Controllers.ContentApi.Models; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.Schemas; @@ -38,23 +39,15 @@ namespace Squidex.Controllers.ContentApi [SwaggerIgnore] public sealed class ContentsController : ControllerBase { - private readonly ISchemaProvider schemas; - private readonly IScriptEngine scriptEngine; - private readonly IContentRepository contentRepository; - private readonly IGraphQLService graphQL; - - public ContentsController( - ICommandBus commandBus, - ISchemaProvider schemas, - IScriptEngine scriptEngine, - IContentRepository contentRepository, - IGraphQLService graphQL) + private readonly IContentQueryService contentQuery; + private readonly IGraphQLService graphQl; + + public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IGraphQLService graphQl) : base(commandBus) { - this.graphQL = graphQL; - this.schemas = schemas; - this.scriptEngine = scriptEngine; - this.contentRepository = contentRepository; + this.contentQuery = contentQuery; + + this.graphQl = graphQl; } [MustBeAppReader] @@ -64,7 +57,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(2)] public async Task PostGraphQL([FromBody] GraphQLQuery query) { - var result = await graphQL.QueryAsync(App, User, query); + var result = await graphQl.QueryAsync(App, User, query); if (result.Errors?.Length > 0) { @@ -82,8 +75,6 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(2)] public async Task GetContents(string name, [FromQuery] string ids = null) { - var schemaEntity = await FindSchemaAsync(name); - var idsList = new HashSet(); if (!string.IsNullOrWhiteSpace(ids)) @@ -99,34 +90,18 @@ namespace Squidex.Controllers.ContentApi var isFrontendClient = User.IsFrontendClient(); - var query = Request.QueryString.ToString(); - - var taskForItems = contentRepository.QueryAsync(App, schemaEntity.Id, isFrontendClient, idsList, query); - var taskForCount = contentRepository.CountAsync(App, schemaEntity.Id, isFrontendClient, idsList, query); - - await Task.WhenAll(taskForItems, taskForCount); - - var scriptText = schemaEntity.ScriptQuery; - - var hasScript = !string.IsNullOrWhiteSpace(scriptText); + var contents = await contentQuery.QueryWithCountAsync(App, name, User, idsList, Request.QueryString.ToString()); var response = new AssetsDto { - Total = taskForCount.Result, - Items = taskForItems.Result.Take(200).Select(item => + Total = contents.Total, + Items = contents.Items.Take(200).Select(item => { var itemModel = SimpleMapper.Map(item, new ContentDto()); if (item.Data != null) { - var data = item.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient); - - if (hasScript && !isFrontendClient) - { - data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = item.Id, User = User }, scriptText); - } - - itemModel.Data = data; + itemModel.Data = item.Data.ToApiModel(contents.SchemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient); } return itemModel; @@ -142,39 +117,18 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task GetContent(string name, Guid id) { - var schemaEntity = await FindSchemaAsync(name); + var content = await contentQuery.FindContentAsync(App, name, User, id); - var entity = await contentRepository.FindContentAsync(App, schemaEntity.Id, id); + var response = SimpleMapper.Map(content.ContentEntity, new ContentDto()); - if (entity == null) - { - return NotFound(); - } - - var response = SimpleMapper.Map(entity, new ContentDto()); - - if (entity.Data != null) + if (content.ContentEntity.Data != null) { var isFrontendClient = User.IsFrontendClient(); - var data = entity.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient); - - if (!isFrontendClient) - { - var scriptText = schemaEntity.ScriptQuery; - - var hasScript = !string.IsNullOrWhiteSpace(scriptText); - - if (hasScript) - { - data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = entity.Id, User = User }, scriptText); - } - } - - response.Data = data; + response.Data = content.ContentEntity.Data.ToApiModel(content.SchemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient); } - Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + Response.Headers["ETag"] = new StringValues(content.ContentEntity.Version.ToString()); return Ok(response); } @@ -185,7 +139,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PostContent(string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { - await FindSchemaAsync(name); + await contentQuery.FindSchemaAsync(App, name); var command = new CreateContent { ContentId = Guid.NewGuid(), User = User, Data = request.ToCleaned(), Publish = publish }; @@ -203,7 +157,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PutContent(string name, Guid id, [FromBody] NamedContentData request) { - await FindSchemaAsync(name); + await contentQuery.FindSchemaAsync(App, name); var command = new UpdateContent { ContentId = id, User = User, Data = request.ToCleaned() }; @@ -221,7 +175,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PatchContent(string name, Guid id, [FromBody] NamedContentData request) { - await FindSchemaAsync(name); + await contentQuery.FindSchemaAsync(App, name); var command = new PatchContent { ContentId = id, User = User, Data = request.ToCleaned() }; @@ -239,7 +193,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PublishContent(string name, Guid id) { - await FindSchemaAsync(name); + await contentQuery.FindSchemaAsync(App, name); var command = new PublishContent { ContentId = id, User = User }; @@ -254,7 +208,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task UnpublishContent(string name, Guid id) { - await FindSchemaAsync(name); + await contentQuery.FindSchemaAsync(App, name); var command = new UnpublishContent { ContentId = id, User = User }; @@ -269,7 +223,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task DeleteContent(string name, Guid id) { - await FindSchemaAsync(name); + await contentQuery.FindSchemaAsync(App, name); var command = new DeleteContent { ContentId = id, User = User }; @@ -277,26 +231,5 @@ namespace Squidex.Controllers.ContentApi return NoContent(); } - - private async Task FindSchemaAsync(string name) - { - ISchemaEntity schemaEntity; - - if (Guid.TryParse(name, out var schemaId)) - { - schemaEntity = await schemas.FindSchemaByIdAsync(schemaId); - } - else - { - schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name); - } - - if (schemaEntity == null || !schemaEntity.IsPublished) - { - throw new DomainObjectNotFoundException(name, typeof(ISchemaEntity)); - } - - return schemaEntity; - } } } diff --git a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs index 437aa691b..b50b2cbcd 100644 --- a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs +++ b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs @@ -34,17 +34,17 @@ namespace Squidex.Pipeline CanGenerateAssetSourceUrl = allowAssetSourceUrl; } - public string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity) + public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) { - if (!assetEntity.IsImage) + if (!asset.IsImage) { return null; } - return urlsOptions.BuildUrl($"api/assets/{assetEntity.Id}?version={assetEntity.Version}&width=100&mode=Max"); + return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}&width=100&mode=Max"); } - public string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity) + public string GenerateAssetUrl(IAppEntity app, IAssetEntity assetEntity) { return urlsOptions.BuildUrl($"api/assets/{assetEntity.Id}?version={assetEntity.Version}"); } diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs index 5bbfb3a24..3d23c9353 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs @@ -17,14 +17,14 @@ namespace Squidex.Domain.Apps.Read.Contents.TestData { public bool CanGenerateAssetSourceUrl { get; } = true; - public string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity) + public string GenerateAssetUrl(IAppEntity app, IAssetEntity assetEntity) { return $"assets/{assetEntity.Id}"; } - public string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity) + public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) { - return $"assets/{assetEntity.Id}?width=100"; + return $"assets/{asset.Id}?width=100"; } public string GenerateAssetSourceUrl(IAppEntity appEntity, IAssetEntity assetEntity)