// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Translations; using Squidex.Shared; #pragma warning disable RECS0147 namespace Squidex.Domain.Apps.Entities.Contents.Queries { public sealed class ContentQueryService : IContentQueryService { private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); private readonly IAppProvider appProvider; private readonly IContentEnricher contentEnricher; private readonly IContentRepository contentRepository; private readonly IContentLoader contentVersionLoader; private readonly ContentQueryParser queryParser; public ContentQueryService( IAppProvider appProvider, IContentEnricher contentEnricher, IContentRepository contentRepository, IContentLoader contentVersionLoader, ContentQueryParser queryParser) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(contentEnricher, nameof(contentEnricher)); Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader)); Guard.NotNull(queryParser, nameof(queryParser)); this.appProvider = appProvider; this.contentEnricher = contentEnricher; this.contentRepository = contentRepository; this.contentVersionLoader = contentVersionLoader; this.queryParser = queryParser; this.queryParser = queryParser; } public async Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1) { Guard.NotNull(context, nameof(context)); if (id == default) { throw new DomainObjectNotFoundException(id.ToString()); } var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context, schema); using (Profiler.TraceMethod()) { IContentEntity? content; if (version > EtagVersion.Empty) { content = await FindByVersionAsync(context, id, version); } else { content = await FindCoreAsync(context, id, schema); } if (content == null || content.SchemaId.Id != schema.Id) { throw new DomainObjectNotFoundException(id.ToString()); } return await TransformAsync(context, content); } } public async Task> QueryAsync(Context context, string schemaIdOrName, Q query) { Guard.NotNull(context, nameof(context)); if (query == null) { return EmptyContents; } var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context, schema); using (Profiler.TraceMethod()) { IResultList contents; if (query.Ids != null && query.Ids.Count > 0) { contents = await QueryByIdsAsync(context, schema, query); } else { contents = await QueryByQueryAsync(context, schema, query); } return await TransformAsync(context, contents); } } public async Task> QueryAsync(Context context, IReadOnlyList ids) { Guard.NotNull(context, nameof(context)); if (ids == null || ids.Count == 0) { return EmptyContents; } using (Profiler.TraceMethod()) { var contents = await QueryCoreAsync(context, ids); var filtered = contents .GroupBy(x => x.Schema.Id) .Select(g => FilterContents(g, context)) .SelectMany(c => c); var results = await TransformCoreAsync(context, filtered); return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); } } private async Task> TransformAsync(Context context, IResultList contents) { var transformed = await TransformCoreAsync(context, contents); return ResultList.Create(contents.Total, transformed); } private async Task TransformAsync(Context context, IContentEntity content) { var transformed = await TransformCoreAsync(context, Enumerable.Repeat(content, 1)); return transformed[0]; } private async Task> TransformCoreAsync(Context context, IEnumerable contents) { using (Profiler.TraceMethod()) { return await contentEnricher.EnrichAsync(contents, context); } } public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName) { ISchemaEntity? schema = null; var canCache = !context.IsFrontendClient; if (Guid.TryParse(schemaIdOrName, out var guid)) { var schemaId = DomainId.Create(guid); schema = await appProvider.GetSchemaAsync(context.App.Id, schemaId, false, canCache); } if (schema == null) { schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName, canCache); } if (schema == null) { throw new DomainObjectNotFoundException(schemaIdOrName); } return schema; } private static void CheckPermission(Context context, params ISchemaEntity[] schemas) { foreach (var schema in schemas) { if (!HasPermission(context, schema)) { throw new DomainForbiddenException(T.Get("schemas.noPermission")); } } } private static IEnumerable FilterContents(IGrouping group, Context context) { var schema = group.First().Schema; if (HasPermission(context, schema)) { return group.Select(x => x.Content); } else { return Enumerable.Empty(); } } private static bool HasPermission(Context context, ISchemaEntity schema) { var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); return context.Permissions.Allows(permission); } private async Task> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query) { var parsedQuery = await queryParser.ParseQueryAsync(context, schema, query); return await QueryCoreAsync(context, schema, parsedQuery, query.Reference); } private async Task> QueryByIdsAsync(Context context, ISchemaEntity schema, Q query) { var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet()); return contents.SortSet(x => x.Id, query.Ids); } private Task> QueryCoreAsync(Context context, IReadOnlyList ids) { return contentRepository.QueryAsync(context.App, new HashSet(ids), context.Scope()); } private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query, DomainId? referenced) { return contentRepository.QueryAsync(context.App, schema, query, referenced, context.Scope()); } private Task> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet ids) { return contentRepository.QueryAsync(context.App, schema, ids, context.Scope()); } private Task FindCoreAsync(Context context, DomainId id, ISchemaEntity schema) { return contentRepository.FindContentAsync(context.App, schema, id, context.Scope()); } private Task FindByVersionAsync(Context context, DomainId id, long version) { return contentVersionLoader.GetAsync(context.App.Id, id, version); } } }