// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using GraphQL; using GraphQL.DataLoader; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Cache; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Infrastructure; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; public sealed class GraphQLExecutionContext : QueryExecutionContext { private const int MaxBatchSize = 5000; private const int MinBatchSize = 1; private static readonly EmptyDataLoaderResult EmptyAssets = new EmptyDataLoaderResult(); private static readonly EmptyDataLoaderResult EmptyContents = new EmptyDataLoaderResult(); private readonly IDataLoaderContextAccessor dataLoaders; private readonly GraphQLOptions options; private readonly int batchSize; public override Context Context { get; } public GraphQLExecutionContext( IDataLoaderContextAccessor dataLoaders, IAssetQueryService assetQuery, IAssetCache assetCache, IContentQueryService contentQuery, IContentCache contentCache, IServiceProvider serviceProvider, Context context, IOptions options) : base(assetQuery, assetCache, contentQuery, contentCache, serviceProvider) { this.dataLoaders = dataLoaders; Context = context.Clone(b => b .WithResolveSchemaNames() .WithNoCleanup() .WithNoEnrichment()); this.options = options.Value; batchSize = Context.BatchSize(); if (batchSize == 0) { batchSize = options.Value.DataLoaderBatchSize; } else { batchSize = Math.Max(MinBatchSize, Math.Min(MaxBatchSize, batchSize)); } } public async ValueTask FindUserAsync(RefToken refToken, CancellationToken ct) { if (refToken.IsClient) { return new ClientUser(refToken); } else { var dataLoader = GetUserLoader(); return await dataLoader.LoadAsync(refToken.Identifier).GetResultAsync(ct); } } public IDataLoaderResult GetContent(DomainId schemaId, DomainId id, long version) { var cacheKey = $"{nameof(GetContent)}_{schemaId}_{id}_{version}"; return dataLoaders.Context!.GetOrAddLoader(cacheKey, ct => { return FindContentAsync(schemaId.ToString(), id, version, ct); }).LoadAsync(); } public IDataLoaderResult GetAsset(DomainId id, TimeSpan cacheDuration) { var assets = GetAssets([id], cacheDuration); var asset = assets.Then(x => x.FirstOrDefault()); return asset; } public IDataLoaderResult GetContent(DomainId schemaId, DomainId id, HashSet? fields, TimeSpan cacheDuration) { var contents = GetContents([id], fields, cacheDuration); var content = contents.Then(x => x.FirstOrDefault(x => x.SchemaId.Id == schemaId)); return content; } public IDataLoaderResult GetAssets(List? ids, TimeSpan cacheDuration) { if (ids is not { Count: > 0 }) { return EmptyAssets; } return GetAssetsLoader().LoadAsync(BuildKeys(ids, cacheDuration)).Then(x => x.NotNull().ToArray()); } public IDataLoaderResult GetContents(List? ids, HashSet? fields, TimeSpan cacheDuration) { if (ids is not { Count: > 0 }) { return EmptyContents; } if (fields == null) { return GetContentsLoader().LoadAsync(BuildKeys(ids, cacheDuration)).Then(x => x.NotNull().ToArray()); } return GetContentsLoaderWithFields().LoadAsync(BuildKeys(ids, fields)).Then(x => x.NotNull().ToArray()); } private IDataLoader, EnrichedAsset> GetAssetsLoader() { return dataLoaders.Context!.GetOrAddCachingLoader(AssetCache, nameof(GetAssetsLoader), async (batch, ct) => { var result = await QueryAssetsByIdsAsync(batch, ct); return result.ToDictionary(x => x.Id); }, maxBatchSize: batchSize); } private IDataLoader, EnrichedContent> GetContentsLoader() { return dataLoaders.Context!.GetOrAddCachingLoader(ContentCache, nameof(GetContentsLoader), async (batch, ct) => { var result = await QueryContentsByIdsAsync(batch, null, ct); return result.ToDictionary(x => x.Id); }, maxBatchSize: batchSize); } private IDataLoader<(DomainId Id, HashSet Fields), EnrichedContent> GetContentsLoaderWithFields() { return dataLoaders.Context!.GetOrAddNonCachingBatchLoader<(DomainId Id, HashSet Fields), EnrichedContent>(nameof(GetContentsLoaderWithFields), async (batch, ct) => { var fields = batch.SelectMany(x => x.Fields).ToHashSet(); var result = await QueryContentsByIdsAsync(batch.Select(x => x.Id), fields, ct); return result.ToDictionary(x => (x.Id, fields)); }, maxBatchSize: batchSize); } private IDataLoader GetUserLoader() { return dataLoaders.Context!.GetOrAddBatchLoader(nameof(GetUserLoader), async (batch, ct) => { var result = await Resolve().QueryManyAsync(batch.ToArray(), ct); return result; }); } private static (DomainId, HashSet)[] BuildKeys(List ids, HashSet fields) { // Use manual loops and arrays to avoid allocations. var keys = new (DomainId, HashSet)[ids.Count]; for (var i = 0; i < ids.Count; i++) { keys[i] = (ids[0], fields); } return keys; } private static CacheableId[] BuildKeys(List ids, TimeSpan cacheDuration) { // Use manual loops and arrays to avoid allocations. var keys = new CacheableId[ids.Count]; for (var i = 0; i < ids.Count; i++) { keys[i] = new CacheableId(ids[i], cacheDuration); } return keys; } }