Browse Source

References endpoints. (#606)

* References endpoints.

* Tests fixed.

* Mini fix.

* Some fallback changes.
pull/607/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
52f8b6cf89
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 40
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs
  3. 120
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  4. 138
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs
  5. 37
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  6. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs
  7. 50
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs
  8. 7
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs
  9. 64
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs
  10. 12
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs
  11. 112
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs
  12. 121
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs
  13. 111
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs
  14. 98
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs
  15. 66
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs
  16. 9
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs
  17. 9
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  19. 45
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  20. 35
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  21. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs
  23. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  24. 121
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  25. 107
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs
  28. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs
  29. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs
  31. 77
      backend/src/Squidex.Domain.Apps.Entities/Q.cs
  32. 2
      backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs
  33. 68
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  34. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs
  35. 26
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs
  36. 42
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs
  37. 33
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  38. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs
  39. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs
  40. 46
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  41. 27
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs
  42. 66
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs
  43. 49
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  44. 50
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs
  45. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs
  46. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs
  47. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs

50
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Log; using Squidex.Log;
@ -82,27 +81,40 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, ClrQuery query) public async Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, Q q)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery"))
{ {
try try
{ {
query = query.AdjustToModel(); if (q.Ids != null && q.Ids.Count > 0)
{
var assetEntities =
await Collection.Find(BuildFilter(appId, q.Ids.ToHashSet())).SortByDescending(x => x.LastModified)
.QueryLimit(q.Query)
.QuerySkip(q.Query)
.ToListAsync();
return ResultList.Create(assetEntities.Count, assetEntities.OfType<IAssetEntity>());
}
else
{
var query = q.Query.AdjustToModel();
var filter = query.BuildFilter(appId, parentId); var filter = query.BuildFilter(appId, parentId);
var assetCount = Collection.Find(filter).CountDocumentsAsync(); var assetCount = Collection.Find(filter).CountDocumentsAsync();
var assetItems = var assetItems =
Collection.Find(filter) Collection.Find(filter)
.QueryLimit(query) .QueryLimit(query)
.QuerySkip(query) .QuerySkip(query)
.QuerySort(query) .QuerySort(query)
.ToListAsync(); .ToListAsync();
var (items, total) = await AsyncHelper.WhenAll(assetItems, assetCount); var (items, total) = await AsyncHelper.WhenAll(assetItems, assetCount);
return ResultList.Create<IAssetEntity>(total, items); return ResultList.Create<IAssetEntity>(total, items);
}
} }
catch (MongoQueryException ex) when (ex.Message.Contains("17406")) catch (MongoQueryException ex) when (ex.Message.Contains("17406"))
{ {
@ -135,18 +147,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, HashSet<DomainId> ids)
{
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByIds"))
{
var assetEntities =
await Collection.Find(BuildFilter(appId, ids)).SortByDescending(x => x.LastModified)
.ToListAsync();
return ResultList.Create(assetEntities.Count, assetEntities.OfType<IAssetEntity>());
}
}
public async Task<IAssetEntity?> FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize) public async Task<IAssetEntity?> FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>()) using (Profiler.TraceMethod<MongoAssetRepository>())

40
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs

@ -1,40 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using MongoDB.Bson.Serialization;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
internal static class Fields
{
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
private static readonly Lazy<string> SchemaIdField = new Lazy<string>(GetSchemaIdField);
private static readonly Lazy<string> StatusField = new Lazy<string>(GetStatusField);
public static string Id => IdField.Value;
public static string SchemaId => SchemaIdField.Value;
public static string Status => StatusField.Value;
private static string GetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Id)).ElementName;
}
private static string GetSchemaIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.IndexedSchemaId)).ElementName;
}
private static string GetStatusField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Status)).ElementName;
}
}
}

120
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -7,7 +7,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
@ -25,24 +24,29 @@ using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
public sealed class MongoContentCollectionAll : MongoRepositoryBase<MongoContentEntity> public sealed class MongoContentCollection : MongoRepositoryBase<MongoContentEntity>
{ {
private readonly QueryContent queryContentAsync; private readonly QueryAsStream queryAsStream;
private readonly QueryContentsByIds queryContentsById; private readonly QueryById queryBdId;
private readonly QueryContentsByQuery queryContentsByQuery; private readonly QueryByIds queryByIds;
private readonly QueryIdsAsync queryIdsAsync; private readonly QueryByQuery queryByQuery;
private readonly QueryReferrersAsync queryReferrersAsync; private readonly QueryReferences queryReferences;
private readonly QueryScheduledContents queryScheduledItems; private readonly QueryReferrers queryReferrers;
private readonly QueryScheduled queryScheduled;
public MongoContentCollectionAll(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter) private readonly string name;
public MongoContentCollection(string name, IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter dataConverter)
: base(database) : base(database)
{ {
queryContentAsync = new QueryContent(converter); this.name = name;
queryContentsById = new QueryContentsByIds(converter, appProvider);
queryContentsByQuery = new QueryContentsByQuery(converter, indexer, appProvider); queryAsStream = new QueryAsStream(dataConverter, appProvider);
queryIdsAsync = new QueryIdsAsync(appProvider); queryBdId = new QueryById(dataConverter);
queryReferrersAsync = new QueryReferrersAsync(); queryByIds = new QueryByIds(dataConverter);
queryScheduledItems = new QueryScheduledContents(); queryByQuery = new QueryByQuery(dataConverter, indexer, appProvider);
queryReferences = new QueryReferences(dataConverter, queryByIds);
queryReferrers = new QueryReferrers(dataConverter);
queryScheduled = new QueryScheduled(dataConverter);
} }
public IMongoCollection<MongoContentEntity> GetInternalCollection() public IMongoCollection<MongoContentEntity> GetInternalCollection()
@ -52,53 +56,63 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
protected override string CollectionName() protected override string CollectionName()
{ {
return "States_Contents_All2"; return name;
} }
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default) protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{ {
await queryContentAsync.PrepareAsync(collection, ct); await queryAsStream.PrepareAsync(collection, ct);
await queryContentsById.PrepareAsync(collection, ct); await queryBdId.PrepareAsync(collection, ct);
await queryContentsByQuery.PrepareAsync(collection, ct); await queryByIds.PrepareAsync(collection, ct);
await queryIdsAsync.PrepareAsync(collection, ct); await queryByQuery.PrepareAsync(collection, ct);
await queryReferrersAsync.PrepareAsync(collection, ct); await queryReferences.PrepareAsync(collection, ct);
await queryScheduledItems.PrepareAsync(collection, ct); await queryReferrers.PrepareAsync(collection, ct);
await queryScheduled.PrepareAsync(collection, ct);
} }
public IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds) public IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds)
{ {
return queryContentsByQuery.StreamAll(appId, schemaIds); return queryAsStream.StreamAll(appId, schemaIds);
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q)
{ {
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await queryContentsByQuery.DoAsync(app, schema, query, referenced, SearchScope.All); if (q.Ids != null && q.Ids.Count > 0)
{
return await queryByIds.QueryAsync(app.Id, schemas, q);
}
if (q.Referencing != default)
{
return await queryReferences.QueryAsync(app.Id, schemas, q);
}
if (q.Reference != default)
{
return await queryByQuery.QueryAsync(app, schemas, q);
}
throw new NotSupportedException();
} }
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<DomainId> ids) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope)
{ {
Guard.NotNull(app, nameof(app)); using (Profiler.TraceMethod<MongoContentRepository>())
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds"))
{ {
var result = await queryContentsById.DoAsync(app.Id, schema, ids, false); if (q.Ids != null && q.Ids.Count > 0l)
{
return ResultList.Create(result.Count, result.Select(x => x.Content)); return await queryByIds.QueryAsync(app.Id, new List<ISchemaEntity> { schema }, q);
} }
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<DomainId> ids)
{
Guard.NotNull(app, nameof(app));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema")) if (q.Referencing == default)
{ {
var result = await queryContentsById.DoAsync(app.Id, null, ids, false); return await queryByQuery.QueryAsync(app, schema, q, scope);
}
return result; throw new NotSupportedException();
} }
} }
@ -106,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await queryContentAsync.DoAsync(schema, id); return await queryBdId.QueryAsync(schema, id);
} }
} }
@ -114,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
await queryScheduledItems.DoAsync(now, callback); await queryScheduled.QueryAsync(now, callback);
} }
} }
@ -122,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await queryIdsAsync.DoAsync(appId, ids); return await queryByIds.QueryIdsAsync(appId, ids);
} }
} }
@ -130,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await queryIdsAsync.DoAsync(appId, schemaId, filterNode); return await queryByQuery.QueryIdsAsync(appId, schemaId, filterNode);
} }
} }
@ -138,18 +152,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await queryReferrersAsync.DoAsync(appId, contentId); return await queryReferrers.CheckExistsAsync(appId, contentId);
} }
} }
public Task ResetScheduledAsync(DomainId documentId) public Task<MongoContentEntity> FindAsync(DomainId documentId)
{ {
return Collection.UpdateOneAsync(x => x.DocumentId == documentId, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt)); return Collection.Find(x => x.DocumentId == documentId).FirstOrDefaultAsync();
} }
public Task<MongoContentEntity> FindAsync(DomainId documentId) public Task ResetScheduledAsync(DomainId documentId)
{ {
return Collection.Find(x => x.DocumentId == documentId).FirstOrDefaultAsync(); return Collection.UpdateOneAsync(x => x.DocumentId == documentId, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt));
} }
public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity) public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity)

138
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs

@ -1,138 +0,0 @@
// ==========================================================================
// 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;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public sealed class MongoContentCollectionPublished : MongoRepositoryBase<MongoContentEntity>
{
private readonly QueryContent queryContentAsync;
private readonly QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery;
private readonly QueryIdsAsync queryIdsAsync;
private readonly QueryReferrersAsync queryReferrersAsync;
public MongoContentCollectionPublished(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter)
: base(database)
{
queryContentAsync = new QueryContent(converter);
queryContentsById = new QueryContentsByIds(converter, appProvider);
queryContentsByQuery = new QueryContentsByQuery(converter, indexer, appProvider);
queryReferrersAsync = new QueryReferrersAsync();
queryIdsAsync = new QueryIdsAsync(appProvider);
}
public IMongoCollection<MongoContentEntity> GetInternalCollection()
{
return Collection;
}
protected override MongoCollectionSettings CollectionSettings()
{
return new MongoCollectionSettings
{
ReadPreference = ReadPreference.SecondaryPreferred.With(TimeSpan.FromMinutes(2))
};
}
protected override string CollectionName()
{
return "States_Contents_Published2";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{
await queryContentAsync.PrepareAsync(collection, ct);
await queryContentsById.PrepareAsync(collection, ct);
await queryContentsByQuery.PrepareAsync(collection, ct);
await queryReferrersAsync.PrepareAsync(collection, ct);
await queryIdsAsync.PrepareAsync(collection, ct);
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced)
{
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery"))
{
return await queryContentsByQuery.DoAsync(app, schema, query, referenced, SearchScope.Published);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<DomainId> ids)
{
Guard.NotNull(app, nameof(app));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds"))
{
var result = await queryContentsById.DoAsync(app.Id, schema, ids, true);
return ResultList.Create(result.Count, result.Select(x => x.Content));
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<DomainId> ids)
{
Guard.NotNull(app, nameof(app));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema"))
{
var result = await queryContentsById.DoAsync(app.Id, null, ids, true);
return result;
}
}
public async Task<IContentEntity?> FindContentAsync(ISchemaEntity schema, DomainId id)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await queryContentAsync.DoAsync(schema, id);
}
}
public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await queryIdsAsync.DoAsync(appId, ids);
}
}
public async Task<bool> HasReferrersAsync(DomainId appId, DomainId contentId)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await queryReferrersAsync.DoAsync(appId, contentId);
}
}
public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity)
{
return Collection.UpsertVersionedAsync(documentId, oldVersion, entity.Version, entity);
}
public Task RemoveAsync(DomainId documentId)
{
return Collection.DeleteOneAsync(x => x.DocumentId == documentId);
}
}
}

37
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -28,8 +28,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly DataConverter converter; private readonly DataConverter converter;
private readonly MongoContentCollectionAll collectionAll; private readonly MongoContentCollection collectionAll;
private readonly MongoContentCollectionPublished collectionPublished; private readonly MongoContentCollection collectionPublished;
static MongoContentRepository() static MongoContentRepository()
{ {
@ -45,8 +45,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
converter = new DataConverter(serializer); converter = new DataConverter(serializer);
collectionAll = new MongoContentCollectionAll(database, appProvider, indexer, converter); collectionAll =
collectionPublished = new MongoContentCollectionPublished(database, appProvider, indexer, converter); new MongoContentCollection(
"States_Contents_All2", database, appProvider, indexer, converter);
collectionPublished =
new MongoContentCollection(
"States_Contents_Published2", database, appProvider, indexer, converter);
} }
public async Task InitializeAsync(CancellationToken ct = default) public async Task InitializeAsync(CancellationToken ct = default)
@ -60,39 +65,27 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionAll.StreamAll(appId, schemaIds); return collectionAll.StreamAll(appId, schemaIds);
} }
public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, SearchScope scope) public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, SearchScope scope)
{
if (scope == SearchScope.All)
{
return collectionAll.QueryAsync(app, schema, query, referenced);
}
else
{
return collectionPublished.QueryAsync(app, schema, query, referenced);
}
}
public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<DomainId> ids, SearchScope scope)
{ {
if (scope == SearchScope.All) if (scope == SearchScope.All)
{ {
return collectionAll.QueryAsync(app, schema, ids); return collectionAll.QueryAsync(app, schemas, q);
} }
else else
{ {
return collectionPublished.QueryAsync(app, schema, ids); return collectionPublished.QueryAsync(app, schemas, q);
} }
} }
public Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<DomainId> ids, SearchScope scope) public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope)
{ {
if (scope == SearchScope.All) if (scope == SearchScope.All)
{ {
return collectionAll.QueryAsync(app, ids); return collectionAll.QueryAsync(app, schema, q, scope);
} }
else else
{ {
return collectionPublished.QueryAsync(app, ids); return collectionPublished.QueryAsync(app, schema, q, scope);
} }
} }

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs

@ -35,13 +35,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return x.Name; return x.Name;
} }
public static Func<PropertyPath, PropertyPath> Path(Schema schema) public static Func<PropertyPath, PropertyPath> Path(Schema? schema)
{ {
return propertyNames => return propertyNames =>
{ {
var result = new List<string>(propertyNames); var result = new List<string>(propertyNames);
if (result.Count > 1) if (result.Count > 1 && schema != null)
{ {
var rootEdmName = result[1].UnescapeEdmField(); var rootEdmName = result[1].UnescapeEdmField();
var rootField = schema.FieldsByName[rootEdmName]; var rootField = schema.FieldsByName[rootEdmName];
@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
}; };
} }
public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema) public static ClrQuery AdjustToModel(this ClrQuery query, Schema? schema)
{ {
var pathConverter = Path(schema); var pathConverter = Path(schema);

50
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs

@ -0,0 +1,50 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
public static class Extensions
{
public sealed class StatusModel
{
[BsonId]
[BsonElement("_id")]
public DomainId DocumentId { get; set; }
[BsonRequired]
[BsonElement("id")]
public DomainId Id { get; set; }
[BsonRequired]
[BsonElement("_si")]
public DomainId IndexedSchemaId { get; set; }
[BsonRequired]
[BsonElement("ss")]
public Status Status { get; set; }
}
public static Task<List<StatusModel>> FindStatusAsync(this IMongoCollection<MongoContentEntity> collection, FilterDefinition<MongoContentEntity> filter)
{
var projections = Builders<MongoContentEntity>.Projection;
return collection.Find(filter)
.Project<StatusModel>(projections
.Include(x => x.Id)
.Include(x => x.IndexedSchemaId)
.Include(x => x.Status))
.ToListAsync();
}
}
}

7
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs

@ -21,6 +21,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
public IMongoCollection<MongoContentEntity> Collection { get; private set; } public IMongoCollection<MongoContentEntity> Collection { get; private set; }
public DataConverter DataConverter { get; }
protected OperationBase(DataConverter dataConverter)
{
DataConverter = dataConverter;
}
public Task PrepareAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default) public Task PrepareAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{ {
Collection = collection; Collection = collection;

64
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs

@ -0,0 +1,64 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
public sealed class QueryAsStream : OperationBase
{
private readonly IAppProvider appProvider;
public QueryAsStream(DataConverter converter, IAppProvider appProvider)
: base(converter)
{
this.appProvider = appProvider;
}
protected override async Task PrepareAsync(CancellationToken ct = default)
{
var indexBySchema =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.IndexedSchemaId));
await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct);
}
public async IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds)
{
var find =
schemaIds != null ?
Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && schemaIds.Contains(x.IndexedSchemaId)) :
Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted);
using (var cursor = await find.ToCursorAsync())
{
while (await cursor.MoveNextAsync())
{
foreach (var entity in cursor.Current)
{
var schema = await appProvider.GetSchemaAsync(appId, entity.SchemaId.Id, false);
if (schema != null)
{
entity.ParseData(schema.SchemaDef, DataConverter);
yield return entity;
}
}
}
}
}
}
}

12
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs

@ -13,16 +13,14 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
internal sealed class QueryContent : OperationBase internal sealed class QueryById : OperationBase
{ {
private readonly DataConverter converter; public QueryById(DataConverter dataConverter)
: base(dataConverter)
public QueryContent(DataConverter converter)
{ {
this.converter = converter;
} }
public async Task<IContentEntity?> DoAsync(ISchemaEntity schema, DomainId id) public async Task<IContentEntity?> QueryAsync(ISchemaEntity schema, DomainId id)
{ {
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return null; return null;
} }
contentEntity?.ParseData(schema.SchemaDef, converter); contentEntity?.ParseData(schema.SchemaDef, DataConverter);
} }
return contentEntity; return contentEntity;

112
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs

@ -0,0 +1,112 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryByIds : OperationBase
{
public QueryByIds(DataConverter dataConverter)
: base(dataConverter)
{
}
public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids)
{
if (ids == null || ids.Count == 0)
{
return new List<(DomainId SchemaId, DomainId Id, Status Status)>();
}
var filter = CreateFilter(appId, null, ids);
var contentItems = await Collection.FindStatusAsync(filter);
return contentItems.Select(x => (x.IndexedSchemaId, x.Id, x.Status)).ToList();
}
public async Task<IResultList<IContentEntity>> QueryAsync(DomainId appId, List<ISchemaEntity> schemas, Q q)
{
Guard.NotNull(q, nameof(q));
if (q.Ids == null || q.Ids.Count == 0)
{
return ResultList.CreateFrom<IContentEntity>(0);
}
var filter = CreateFilter(appId, schemas.Select(x => x.Id), q.Ids.ToHashSet());
var items = await FindContentsAsync(q.Query, filter);
if (items.Count > 0)
{
var contentSchemas = schemas.ToDictionary(x => x.Id);
foreach (var content in items)
{
var schema = contentSchemas[content.SchemaId.Id];
content.ParseData(schema.SchemaDef, DataConverter);
}
}
return ResultList.Create(items.Count, items);
}
private async Task<List<MongoContentEntity>> FindContentsAsync(ClrQuery query, FilterDefinition<MongoContentEntity> filter)
{
var result =
Collection.Find(filter)
.QueryLimit(query)
.QuerySkip(query)
.ToListAsync();
return await result;
}
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, IEnumerable<DomainId>? schemaIds, HashSet<DomainId> ids)
{
var filters = new List<FilterDefinition<MongoContentEntity>>();
var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList();
if (documentIds.Count > 1)
{
filters.Add(
Filter.Or(
Filter.In(x => x.DocumentId, documentIds)));
}
else
{
var first = documentIds.First();
filters.Add(
Filter.Or(
Filter.Eq(x => x.DocumentId, first)));
}
if (schemaIds != null)
{
filters.Add(Filter.In(x => x.IndexedSchemaId, schemaIds));
}
filters.Add(Filter.Ne(x => x.IsDeleted, true));
return Filter.And(filters);
}
}
}

121
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs

@ -5,12 +5,14 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
@ -23,9 +25,8 @@ using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
internal sealed class QueryContentsByQuery : OperationBase internal sealed class QueryByQuery : OperationBase
{ {
private readonly DataConverter converter;
private readonly ITextIndex indexer; private readonly ITextIndex indexer;
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
@ -39,10 +40,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
public MongoContentEntity[] Joined { get; set; } public MongoContentEntity[] Joined { get; set; }
} }
public QueryContentsByQuery(DataConverter converter, ITextIndex indexer, IAppProvider appProvider) public QueryByQuery(DataConverter dataConverter, ITextIndex indexer, IAppProvider appProvider)
: base(dataConverter)
{ {
this.converter = converter;
this.indexer = indexer; this.indexer = indexer;
this.appProvider = appProvider; this.appProvider = appProvider;
} }
@ -67,41 +69,89 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct); await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct);
} }
public async IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds) public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode)
{ {
var find = Guard.NotNull(filterNode, nameof(filterNode));
schemaIds != null ?
Collection.Find(x => x.IndexedAppId == appId && schemaIds.Contains(x.IndexedSchemaId) && !x.IsDeleted) :
Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted);
using (var cursor = await find.ToCursorAsync()) try
{ {
while (await cursor.MoveNextAsync()) var schema = await appProvider.GetSchemaAsync(appId, schemaId, false);
if (schema == null)
{ {
foreach (var entity in cursor.Current) return new List<(DomainId SchemaId, DomainId Id, Status Status)>();
{ }
var schema = await appProvider.GetSchemaAsync(appId, entity.SchemaId.Id, false);
var filter = BuildFilter(appId, schemaId, filterNode.AdjustToModel(schema.SchemaDef));
if (schema != null) var contentItems = await Collection.FindStatusAsync(filter);
{
entity.ParseData(schema.SchemaDef, converter);
yield return entity; return contentItems.Select(x => (x.IndexedSchemaId, x.Id, x.Status)).ToList();
} }
catch (MongoCommandException ex) when (ex.Code == 96)
{
throw new DomainException(T.Get("common.resultTooLarge"));
}
catch (MongoQueryException ex) when (ex.Message.Contains("17406"))
{
throw new DomainException(T.Get("common.resultTooLarge"));
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(q, nameof(q));
try
{
var query = q.Query.AdjustToModel(null);
List<DomainId>? fullTextIds = null;
if (!string.IsNullOrWhiteSpace(query.FullText))
{
throw new NotSupportedException();
}
var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), fullTextIds, query, q.Reference);
var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = FindContentsAsync(query, filter);
var (items, total) = await AsyncHelper.WhenAll(contentItems, contentCount);
if (items.Count > 0)
{
var contentSchemas = schemas.ToDictionary(x => x.Id);
foreach (var entity in items)
{
entity.ParseData(contentSchemas[entity.IndexedSchemaId].SchemaDef, DataConverter);
} }
} }
return ResultList.Create<IContentEntity>(total, items);
}
catch (MongoCommandException ex) when (ex.Code == 96)
{
throw new DomainException(T.Get("common.resultTooLarge"));
}
catch (MongoQueryException ex) when (ex.Message.Contains("17406"))
{
throw new DomainException(T.Get("common.resultTooLarge"));
} }
} }
public async Task<IResultList<IContentEntity>> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, SearchScope scope) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope)
{ {
Guard.NotNull(app, nameof(app)); Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
Guard.NotNull(query, nameof(query)); Guard.NotNull(q, nameof(q));
try try
{ {
query = query.AdjustToModel(schema.SchemaDef); var query = q.Query.AdjustToModel(schema.SchemaDef);
List<DomainId>? fullTextIds = null; List<DomainId>? fullTextIds = null;
@ -117,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
} }
} }
var filter = CreateFilter(schema.AppId.Id, schema.Id, fullTextIds, query, referenced); var filter = CreateFilter(schema.AppId.Id, Enumerable.Repeat(schema.Id, 1), fullTextIds, query, q.Reference);
var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = FindContentsAsync(query, filter); var contentItems = FindContentsAsync(query, filter);
@ -126,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
foreach (var entity in items) foreach (var entity in items)
{ {
entity.ParseData(schema.SchemaDef, converter); entity.ParseData(schema.SchemaDef, DataConverter);
} }
return ResultList.Create<IContentEntity>(total, items); return ResultList.Create<IContentEntity>(total, items);
@ -180,7 +230,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return query.Sort?.All(x => x.Path.ToString() == "mt" && x.Order == SortOrder.Descending) == true; return query.Sort?.All(x => x.Path.ToString() == "mt" && x.Order == SortOrder.Descending) == true;
} }
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, DomainId schemaId, ICollection<DomainId>? ids, ClrQuery? query, DomainId? referenced) private static FilterDefinition<MongoContentEntity> BuildFilter(DomainId appId, DomainId schemaId, FilterNode<ClrValue>? filterNode)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
@ -189,6 +239,23 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
Filter.Ne(x => x.IsDeleted, true) Filter.Ne(x => x.IsDeleted, true)
}; };
if (filterNode != null)
{
filters.Add(filterNode.BuildFilter<MongoContentEntity>());
}
return Filter.And(filters);
}
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, IEnumerable<DomainId> schemaIds, ICollection<DomainId>? ids, ClrQuery? query, DomainId referenced)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IndexedAppId, appId),
Filter.In(x => x.IndexedSchemaId, schemaIds),
Filter.Ne(x => x.IsDeleted, true)
};
if (ids != null && ids.Count > 0) if (ids != null && ids.Count > 0)
{ {
var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList(); var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList();
@ -204,9 +271,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
filters.Add(query.Filter.BuildFilter<MongoContentEntity>()); filters.Add(query.Filter.BuildFilter<MongoContentEntity>());
} }
if (referenced != null) if (referenced != default)
{ {
filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced.Value)); filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced));
} }
return Filter.And(filters); return Filter.And(filters);

111
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs

@ -1,111 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryContentsByIds : OperationBase
{
private readonly DataConverter converter;
private readonly IAppProvider appProvider;
public QueryContentsByIds(DataConverter converter, IAppProvider appProvider)
{
this.converter = converter;
this.appProvider = appProvider;
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> DoAsync(DomainId appId, ISchemaEntity? schema, HashSet<DomainId> ids, bool canCache)
{
Guard.NotNull(ids, nameof(ids));
var find = Collection.Find(CreateFilter(appId, ids));
var contentItems = await find.ToListAsync();
var contentSchemas = await GetSchemasAsync(appId, schema, contentItems, canCache);
var result = new List<(IContentEntity Content, ISchemaEntity Schema)>();
foreach (var contentEntity in contentItems)
{
if (contentSchemas.TryGetValue(contentEntity.IndexedSchemaId, out var contentSchema))
{
contentEntity.ParseData(contentSchema.SchemaDef, converter);
result.Add((contentEntity, contentSchema));
}
}
return result;
}
private async Task<IDictionary<DomainId, ISchemaEntity>> GetSchemasAsync(DomainId appId, ISchemaEntity? schema, List<MongoContentEntity> contentItems, bool canCache)
{
var schemas = new Dictionary<DomainId, ISchemaEntity>();
if (schema != null)
{
schemas[schema.Id] = schema;
}
var schemaIds = contentItems.Select(x => x.IndexedSchemaId).Distinct();
foreach (var schemaId in schemaIds)
{
if (!schemas.ContainsKey(schemaId))
{
var found = await appProvider.GetSchemaAsync(appId, schemaId, false, canCache);
if (found != null)
{
schemas[schemaId] = found;
}
}
}
return schemas;
}
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, ICollection<DomainId> ids)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Ne(x => x.IsDeleted, true)
};
if (ids != null && ids.Count > 0)
{
var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList();
if (ids.Count > 1)
{
filters.Add(
Filter.Or(
Filter.In(x => x.DocumentId, documentIds)));
}
else
{
var first = documentIds.First();
filters.Add(
Filter.Or(
Filter.Eq(x => x.DocumentId, first)));
}
}
return Filter.And(filters);
}
}
}

98
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs

@ -1,98 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryIdsAsync : OperationBase
{
private static readonly List<(DomainId SchemaId, DomainId Id, Status Status)> EmptyIds = new List<(DomainId SchemaId, DomainId Id, Status Status)>();
private readonly IAppProvider appProvider;
public QueryIdsAsync(IAppProvider appProvider)
{
this.appProvider = appProvider;
}
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
}
public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> DoAsync(DomainId appId, HashSet<DomainId> ids)
{
var documentIds = ids.Select(x => DomainId.Combine(appId, x));
var filter =
Filter.And(
Filter.In(x => x.DocumentId, documentIds),
Filter.Ne(x => x.IsDeleted, true));
return await SearchAsync(filter);
}
public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> DoAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode)
{
var schema = await appProvider.GetSchemaAsync(appId, schemaId, false);
if (schema == null)
{
return EmptyIds;
}
var filter = BuildFilter(filterNode.AdjustToModel(schema.SchemaDef), appId, schemaId);
return await SearchAsync(filter);
}
private async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> SearchAsync(FilterDefinition<MongoContentEntity> filter)
{
var contentEntities =
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId, x => x.Status)
.ToListAsync();
return contentEntities.Select(x => (
DomainId.Create(x[Fields.SchemaId].AsString),
DomainId.Create(x[Fields.Id].AsString),
new Status(x[Fields.Status].AsString)
)).ToList();
}
private static FilterDefinition<MongoContentEntity> BuildFilter(FilterNode<ClrValue>? filterNode, DomainId appId, DomainId schemaId)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IndexedAppId, appId),
Filter.Eq(x => x.IndexedSchemaId, schemaId),
Filter.Ne(x => x.IsDeleted, true)
};
if (filterNode != null)
{
filters.Add(filterNode.BuildFilter<MongoContentEntity>());
}
return Filter.And(filters);
}
}
}

66
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs

@ -0,0 +1,66 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal class QueryReferences : OperationBase
{
private static readonly IResultList<IContentEntity> EmptyIds = ResultList.CreateFrom<IContentEntity>(0);
private readonly QueryByIds queryByIds;
public sealed class ReferencedIdsOnly
{
[BsonId]
[BsonElement("_id")]
public DomainId DocumentId { get; set; }
[BsonRequired]
[BsonElement("rf")]
public HashSet<DomainId>? ReferencedIds { get; set; }
}
public QueryReferences(DataConverter dataConverter, QueryByIds queryByIds)
: base(dataConverter)
{
this.queryByIds = queryByIds;
}
public async Task<IResultList<IContentEntity>> QueryAsync(DomainId appId, List<ISchemaEntity> schemas, Q q)
{
var documentId = DomainId.Combine(appId, q.Referencing);
var find =
Collection
.Find(x => x.DocumentId == documentId)
.Project<ReferencedIdsOnly>(Projection.Include(x => x.ReferencedIds));
var contentEntity = await find.FirstOrDefaultAsync();
if (contentEntity == null)
{
throw new DomainObjectNotFoundException(q.Referencing.ToString());
}
if (contentEntity.ReferencedIds == null || contentEntity.ReferencedIds.Count == 0)
{
return EmptyIds;
}
q = q.WithReferencing(default).WithIds(contentEntity.ReferencedIds!);
return await queryByIds.QueryAsync(appId, schemas, q);
}
}
}

9
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs

@ -13,8 +13,13 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
internal sealed class QueryReferrersAsync : OperationBase internal sealed class QueryReferrers : OperationBase
{ {
public QueryReferrers(DataConverter dataConverter)
: base(dataConverter)
{
}
protected override Task PrepareAsync(CancellationToken ct = default) protected override Task PrepareAsync(CancellationToken ct = default)
{ {
var index = var index =
@ -26,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
} }
public async Task<bool> DoAsync(DomainId appId, DomainId contentId) public async Task<bool> CheckExistsAsync(DomainId appId, DomainId contentId)
{ {
var filter = var filter =
Filter.And( Filter.And(

9
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs

@ -16,8 +16,13 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
internal sealed class QueryScheduledContents : OperationBase internal sealed class QueryScheduled : OperationBase
{ {
public QueryScheduled(DataConverter dataConverter)
: base(dataConverter)
{
}
protected override Task PrepareAsync(CancellationToken ct = default) protected override Task PrepareAsync(CancellationToken ct = default)
{ {
var index = var index =
@ -28,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
} }
public Task DoAsync(Instant now, Func<IContentEntity, Task> callback) public Task QueryAsync(Instant now, Func<IContentEntity, Task> callback)
{ {
Guard.NotNull(callback, nameof(callback)); Guard.NotNull(callback, nameof(callback));

2
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public interface IAssetQueryService public interface IAssetQueryService
{ {
Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q query); Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q q);
Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId); Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId);

45
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs

@ -46,60 +46,49 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
this.options = options.Value; this.options = options.Value;
} }
public virtual async ValueTask<ClrQuery> ParseQueryAsync(Context context, Q q) public virtual async ValueTask<Q> ParseQueryAsync(Context context, Q q)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(q, nameof(q)); Guard.NotNull(q, nameof(q));
using (Profiler.TraceMethod<AssetQueryParser>()) using (Profiler.TraceMethod<AssetQueryParser>())
{ {
ClrQuery result; var query = q.Query;
if (q.Query != null) if (!string.IsNullOrWhiteSpace(q?.JsonQueryString))
{ {
result = q.Query; query = ParseJson(q.JsonQueryString);
} }
else else if (!string.IsNullOrWhiteSpace(q?.ODataQuery))
{ {
if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) query = ParseOData(q.ODataQuery);
{
result = ParseJson(q.JsonQuery);
}
else if (!string.IsNullOrWhiteSpace(q?.ODataQuery))
{
result = ParseOData(q.ODataQuery);
}
else
{
result = new ClrQuery();
}
} }
if (result.Filter != null) if (query.Filter != null)
{ {
result.Filter = await FilterTagTransformer.TransformAsync(result.Filter, context.App.Id, tagService); query.Filter = await FilterTagTransformer.TransformAsync(query.Filter, context.App.Id, tagService);
} }
if (result.Sort.Count == 0) if (query.Sort.Count == 0)
{ {
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); query.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending));
} }
if (!result.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase)))
{ {
result.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending)); query.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending));
} }
if (result.Take == long.MaxValue) if (query.Take == long.MaxValue)
{ {
result.Take = options.DefaultPageSize; query.Take = options.DefaultPageSize;
} }
else if (result.Take > options.MaxResults) else if (query.Take > options.MaxResults)
{ {
result.Take = options.MaxResults; query.Take = options.MaxResults;
} }
return result; return q!.WithQuery(query);
} }
} }

35
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -94,44 +94,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return assetFolders; return assetFolders;
} }
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q query) public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q q)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query)); Guard.NotNull(q, nameof(q));
IResultList<IAssetEntity> assets; q = await queryParser.ParseQueryAsync(context, q);
if (query.Ids != null && query.Ids.Count > 0) var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q);
{
assets = await QueryByIdsAsync(context, query); if (q.Ids != null && q.Ids.Count > 0)
}
else
{ {
assets = await QueryByQueryAsync(context, parentId, query); assets = assets.SortSet(x => x.Id, q.Ids);
} }
var enriched = await assetEnricher.EnrichAsync(assets, context); var enriched = await assetEnricher.EnrichAsync(assets, context);
return ResultList.Create(assets.Total, enriched); return ResultList.Create(assets.Total, enriched);
} }
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(Context context, DomainId? parentId, Q query)
{
var parsedQuery = await queryParser.ParseQueryAsync(context, query);
return await assetRepository.QueryAsync(context.App.Id, parentId, parsedQuery);
}
private async Task<IResultList<IAssetEntity>> QueryByIdsAsync(Context context, Q query)
{
var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<DomainId>(query.Ids));
return Sort(assets, query.Ids);
}
private static IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<DomainId> ids)
{
return assets.SortSet(x => x.Id, ids);
}
} }
} }

5
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -8,7 +8,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Repositories namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
@ -16,9 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
IAsyncEnumerable<IAssetEntity> StreamAll(DomainId appId); IAsyncEnumerable<IAssetEntity> StreamAll(DomainId appId);
Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, ClrQuery query); Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, Q q);
Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, HashSet<DomainId> ids);
Task<IReadOnlyList<DomainId>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids); Task<IReadOnlyList<DomainId>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids);

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var appId = context.App.NamedId(); var appId = context.App.NamedId();
var contents = await contentQuery.QueryAsync(context, ids); var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids));
foreach (var content in contents) foreach (var content in contents)
{ {

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentQueryService public interface IContentQueryService
{ {
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, IReadOnlyList<DomainId> ids); Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, Q q);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query); Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query);

121
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -35,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public class ContentQueryParser public class ContentQueryParser
{ {
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null);
private readonly JsonSchema genericJsonSchema = BuildJsonSchema("Content", null);
private readonly IMemoryCache cache; private readonly IMemoryCache cache;
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly ContentOptions options; private readonly ContentOptions options;
@ -50,78 +52,68 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.options = options.Value; this.options = options.Value;
} }
public virtual ValueTask<ClrQuery> ParseQueryAsync(Context context, ISchemaEntity schema, Q q) public virtual ValueTask<Q> ParseAsync(Context context, Q q, ISchemaEntity? schema = null)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(q, nameof(q)); Guard.NotNull(q, nameof(q));
using (Profiler.TraceMethod<ContentQueryParser>()) using (Profiler.TraceMethod<ContentQueryParser>())
{ {
ClrQuery result; var query = q.Query;
if (q.Query != null) if (!string.IsNullOrWhiteSpace(q.JsonQueryString))
{ {
result = q.Query; query = ParseJson(context, schema, q.JsonQueryString);
} }
else else if (q?.JsonQuery != null)
{ {
if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) query = ParseJson(context, schema, q.JsonQuery);
{ }
result = ParseJson(context, schema, q.JsonQuery); else if (!string.IsNullOrWhiteSpace(q?.ODataQuery))
} {
else if (q?.ParsedJsonQuery != null) query = ParseOData(context, schema, q.ODataQuery);
{
result = ParseJson(context, schema, q.ParsedJsonQuery);
}
else if (!string.IsNullOrWhiteSpace(q?.ODataQuery))
{
result = ParseOData(context, schema, q.ODataQuery);
}
else
{
result = new ClrQuery();
}
} }
if (result.Sort.Count == 0) if (query.Sort.Count == 0)
{ {
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); query.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending));
} }
if (!result.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase)))
{ {
result.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending)); query.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending));
} }
if (result.Take == long.MaxValue) if (query.Take == long.MaxValue)
{ {
result.Take = options.DefaultPageSize; query.Take = options.DefaultPageSize;
} }
else if (result.Take > options.MaxResults) else if (query.Take > options.MaxResults)
{ {
result.Take = options.MaxResults; query.Take = options.MaxResults;
} }
return new ValueTask<ClrQuery>(result); q = q!.WithQuery(query);
return new ValueTask<Q>(q);
} }
} }
private ClrQuery ParseJson(Context context, ISchemaEntity schema, Query<IJsonValue> query) private ClrQuery ParseJson(Context context, ISchemaEntity? schema, Query<IJsonValue> query)
{ {
var jsonSchema = BuildJsonSchema(context, schema); var jsonSchema = BuildJsonSchema(context, schema);
return jsonSchema.Convert(query); return jsonSchema.Convert(query);
} }
private ClrQuery ParseJson(Context context, ISchemaEntity schema, string json) private ClrQuery ParseJson(Context context, ISchemaEntity? schema, string json)
{ {
var jsonSchema = BuildJsonSchema(context, schema); var jsonSchema = BuildJsonSchema(context, schema);
return jsonSchema.Parse(json, jsonSerializer); return jsonSchema.Parse(json, jsonSerializer);
} }
private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata) private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata)
{ {
try try
{ {
@ -139,8 +131,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
} }
private JsonSchema BuildJsonSchema(Context context, ISchemaEntity schema) private JsonSchema BuildJsonSchema(Context context, ISchemaEntity? schema)
{ {
if (schema == null)
{
return genericJsonSchema;
}
var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient);
var result = cache.GetOrCreate(cacheKey, entry => var result = cache.GetOrCreate(cacheKey, entry =>
@ -153,8 +150,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return result; return result;
} }
private IEdmModel BuildEdmModel(Context context, ISchemaEntity schema) private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema)
{ {
if (schema == null)
{
return genericEdmModel;
}
var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient);
var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry => var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry =>
@ -171,7 +173,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields); var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields);
return ContentSchemaBuilder.CreateContentSchema(schema, dataSchema); return BuildJsonSchema(schema.DisplayName(), dataSchema);
}
private static JsonSchema BuildJsonSchema(string name, JsonSchema? dataSchema)
{
var schema = new JsonSchema
{
Properties =
{
["id"] = SchemaBuilder.StringProperty($"The id of the {name} content.", true),
["version"] = SchemaBuilder.NumberProperty($"The version of the {name}.", true),
["created"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been created.", true),
["createdBy"] = SchemaBuilder.StringProperty($"The user that has created the {name} content.", true),
["lastModified"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been modified last.", true),
["lastModifiedBy"] = SchemaBuilder.StringProperty($"The user that has updated the {name} content last.", true),
["newStatus"] = SchemaBuilder.StringProperty($"The new status of the content.", false),
["status"] = SchemaBuilder.StringProperty($"The status of the content.", true)
},
Type = JsonObjectType.Object
};
if (dataSchema != null)
{
schema.Properties["data"] = SchemaBuilder.ObjectProperty(dataSchema, $"The data of the {name}.", true);
schema.Properties["dataDraft"] = SchemaBuilder.ObjectProperty(dataSchema, $"The draft data of the {name}.");
}
return schema;
} }
private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields)
@ -207,7 +236,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory); var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory);
var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); return BuildEdmModel(app.Name.ToPascalCase(), schema.Name, model, schemaType);
}
private static EdmModel BuildEdmModel(string modelName, string name, EdmModel model, EdmComplexType? schemaType)
{
var entityType = new EdmEntityType(modelName, name);
entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
@ -216,14 +251,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
entityType.AddStructuralProperty(nameof(IContentEntity.NewStatus).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.NewStatus).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32);
entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false));
if (schemaType != null)
{
entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false));
entityType.AddStructuralProperty("dataDraft", new EdmComplexTypeReference(schemaType, false));
model.AddElement(schemaType);
}
var container = new EdmEntityContainer("Squidex", "Container"); var container = new EdmEntityContainer("Squidex", "Container");
container.AddEntitySet("ContentSet", entityType); container.AddEntitySet("ContentSet", entityType);
model.AddElement(container); model.AddElement(container);
model.AddElement(schemaType);
model.AddElement(entityType); model.AddElement(entityType);
return model; return model;

107
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -12,13 +12,10 @@ using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Log; using Squidex.Log;
using Squidex.Shared; using Squidex.Shared;
#pragma warning disable RECS0147
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public sealed class ContentQueryService : IContentQueryService public sealed class ContentQueryService : IContentQueryService
@ -62,8 +59,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {
IContentEntity? content; IContentEntity? content;
@ -86,58 +81,60 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
} }
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query) public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q q)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
if (query == null) if (q == null)
{ {
return EmptyContents; return EmptyContents;
} }
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {
IResultList<IContentEntity> contents; q = await queryParser.ParseAsync(context, q, schema);
if (query.Ids != null && query.Ids.Count > 0) var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope());
{
contents = await QueryByIdsAsync(context, schema, query); if (q.Ids != null && q.Ids.Count > 0)
}
else
{ {
contents = await QueryByQueryAsync(context, schema, query); contents = contents.SortSet(x => x.Id, q.Ids);
} }
return await TransformAsync(context, contents); return await TransformAsync(context, contents);
} }
} }
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, IReadOnlyList<DomainId> ids) public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, Q q)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
if (ids == null || ids.Count == 0) if (q == null)
{
return EmptyContents;
}
var schemas = await GetSchemasAsync(context);
if (schemas.Count == 0)
{ {
return EmptyContents; return EmptyContents;
} }
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {
var contents = await QueryCoreAsync(context, ids); q = await queryParser.ParseAsync(context, q);
var filtered = var contents = await contentRepository.QueryAsync(context.App, schemas, q, context.Scope());
contents
.GroupBy(x => x.Schema.Id)
.Select(g => FilterContents(g, context))
.SelectMany(c => c);
var results = await TransformCoreAsync(context, filtered); if (q.Ids != null && q.Ids.Count > 0)
{
contents = contents.SortSet(x => x.Id, q.Ids);
}
return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); return await TransformAsync(context, contents);
} }
} }
@ -186,70 +183,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
throw new DomainObjectNotFoundException(schemaIdOrName); throw new DomainObjectNotFoundException(schemaIdOrName);
} }
return schema; if (!HasPermission(context, 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"));
{
throw new DomainForbiddenException(T.Get("schemas.noPermission"));
}
} }
return schema;
} }
private static IEnumerable<IContentEntity> FilterContents(IGrouping<DomainId, (IContentEntity Content, ISchemaEntity Schema)> group, Context context) private async Task<List<ISchemaEntity>> GetSchemasAsync(Context context)
{ {
var schema = group.First().Schema; var schemas = await appProvider.GetSchemasAsync(context.App.Id);
if (HasPermission(context, schema)) return schemas.Where(x => HasPermission(context, x)).ToList();
{
return group.Select(x => x.Content);
}
else
{
return Enumerable.Empty<IContentEntity>();
}
} }
private static bool HasPermission(Context context, ISchemaEntity schema) private static bool HasPermission(Context context, ISchemaEntity schema)
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); var permission = Permissions.ForApp(Permissions.AppContentsRead, context.App.Name, schema.SchemaDef.Name);
return context.Permissions.Allows(permission); return context.Permissions.Allows(permission);
} }
private async Task<IResultList<IContentEntity>> 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<IResultList<IContentEntity>> 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<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryCoreAsync(Context context, IReadOnlyList<DomainId> ids)
{
return contentRepository.QueryAsync(context.App, new HashSet<DomainId>(ids), context.Scope());
}
private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query, DomainId? referenced)
{
return contentRepository.QueryAsync(context.App, schema, query, referenced, context.Scope());
}
private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet<DomainId> ids)
{
return contentRepository.QueryAsync(context.App, schema, ids, context.Scope());
}
private Task<IContentEntity?> FindCoreAsync(Context context, DomainId id, ISchemaEntity schema) private Task<IContentEntity?> FindCoreAsync(Context context, DomainId id, ISchemaEntity schema)
{ {
return contentRepository.FindContentAsync(context.App, schema, id, context.Scope()); return contentRepository.FindContentAsync(context.App, schema, id, context.Scope());

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -180,7 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(); await maxRequests.WaitAsync();
try try
{ {
contents = await contentQuery.QueryAsync(context, notLoadedContents); contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedContents));
} }
finally finally
{ {

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs

@ -153,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
return EmptyContents; return EmptyContents;
} }
var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), ids.ToList()); var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), Q.Empty.WithIds(ids));
return references.ToLookup(x => x.Id); return references.ToLookup(x => x.Id);
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var domainId = DomainId.Create(id); var domainId = DomainId.Create(id);
var domainIds = new List<DomainId> { domainId }; var domainIds = new List<DomainId> { domainId };
var references = await contentQueryService.QueryAsync(appContext, domainIds); var references = await contentQueryService.QueryAsync(appContext, Q.Empty.WithIds(domainIds));
var reference = references.FirstOrDefault(); var reference = references.FirstOrDefault();
if (reference != null) if (reference != null)

6
backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -21,11 +21,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{ {
IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds); IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds);
Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<DomainId> ids, SearchScope scope); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, SearchScope scope);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<DomainId> ids, SearchScope scope); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, SearchScope scope);
Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode); Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode);

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs

@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation
{ {
var checkAssets = new CheckAssets(async ids => var checkAssets = new CheckAssets(async ids =>
{ {
return await assetRepository.QueryAsync(context.AppId.Id, new HashSet<DomainId>(ids)); return await assetRepository.QueryAsync(context.AppId.Id, null, Q.Empty.WithIds(ids));
}); });
yield return new AssetsValidator(isRequired, assetsField.Properties, checkAssets); yield return new AssetsValidator(isRequired, assetsField.Properties, checkAssets);

77
backend/src/Squidex.Domain.Apps.Entities/Q.cs

@ -13,75 +13,88 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public sealed class Q : Cloneable<Q> public record Q
{ {
public static readonly Q Empty = new Q(); public static Q Empty => new Q();
public IReadOnlyList<DomainId> Ids { get; private set; } public IReadOnlyList<DomainId>? Ids { get; init; }
public DomainId? Reference { get; private set; } public DomainId Referencing { get; init; }
public string? ODataQuery { get; private set; } public DomainId Reference { get; init; }
public string? JsonQuery { get; private set; } public string? ODataQuery { get; init; }
public Query<IJsonValue>? ParsedJsonQuery { get; private set; } public string? JsonQueryString { get; init; }
public ClrQuery? Query { get; private set; } public Query<IJsonValue>? JsonQuery { get; init; }
public Q WithQuery(ClrQuery? query) public ClrQuery Query { get; init; } = new ClrQuery();
private Q()
{ {
return Clone(clone => clone.Query = query);
} }
public Q WithODataQuery(string? odataQuery) public Q WithQuery(ClrQuery query)
{ {
return Clone(clone => clone.ODataQuery = odataQuery); Guard.NotNull(query, nameof(query));
return this with { Query = query };
} }
public Q WithJsonQuery(string? jsonQuery) public Q WithODataQuery(string? query)
{ {
return Clone(clone => clone.JsonQuery = jsonQuery); return this with { ODataQuery = query };
} }
public Q WithJsonQuery(Query<IJsonValue>? jsonQuery) public Q WithJsonQuery(string? query)
{ {
return Clone(clone => clone.ParsedJsonQuery = jsonQuery); return this with { JsonQueryString = query };
} }
public Q WithIds(params DomainId[] ids) public Q WithJsonQuery(Query<IJsonValue>? query)
{
return this with { JsonQuery = query };
}
public Q WithReferencing(DomainId id)
{
return this with { Referencing = id };
}
public Q WithReference(DomainId id)
{ {
return Clone(clone => clone.Ids = ids.ToList()); return this with { Reference = id };
} }
public Q WithReference(DomainId? reference) public Q WithIds(params DomainId[] ids)
{ {
return Clone(clone => clone.Reference = reference); return this with { Ids = ids?.ToList() };
} }
public Q WithIds(IEnumerable<DomainId> ids) public Q WithIds(IEnumerable<DomainId> ids)
{ {
return Clone(clone => clone.Ids = ids.ToList()); return this with { Ids = ids?.ToList() };
} }
public Q WithIds(string? ids) public Q WithIds(string? ids)
{ {
if (!string.IsNullOrEmpty(ids)) if (string.IsNullOrWhiteSpace(ids))
{ {
return Clone(clone => return this with { Ids = null };
{ }
var idsList = new List<DomainId>();
foreach (var id in ids.Split(',')) var idsList = new List<DomainId>();
{
idsList.Add(DomainId.Create(id));
}
clone.Ids = idsList; if (!string.IsNullOrEmpty(ids))
}); {
foreach (var id in ids.Split(','))
{
idsList.Add(DomainId.Create(id));
}
} }
return this; return this with { Ids = idsList };
} }
} }
} }

2
backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs

@ -84,7 +84,7 @@ namespace Squidex.Infrastructure.Queries.Json
} }
} }
private static Query<IJsonValue> ParseFromJson(string json, IJsonSerializer jsonSerializer) public static Query<IJsonValue> ParseFromJson(string json, IJsonSerializer jsonSerializer)
{ {
try try
{ {

68
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -154,7 +154,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids) public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids)
{ {
var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids));
var response = Deferred.AsyncResponse(() => var response = Deferred.AsyncResponse(() =>
{ {
@ -183,7 +183,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query) public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query)
{ {
var contents = await contentQuery.QueryAsync(Context, query.Ids); var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(query.Ids));
var response = Deferred.AsyncResponse(() => var response = Deferred.AsyncResponse(() =>
{ {
@ -273,7 +273,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)] [ProducesResponseType(typeof(ContentDto), 200)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, DomainId id) public async Task<IActionResult> GetContent(string app, string name, DomainId id)
@ -285,6 +285,68 @@ namespace Squidex.Areas.Api.Controllers.Contents
return Ok(response); return Ok(response);
} }
/// <summary>
/// Get all references of a content.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param>
/// <param name="q">The optional json query.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/{id}/references")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetReferences(string app, string name, DomainId id, [FromQuery] string? q = null)
{
var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id));
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow);
});
return Ok(response);
}
/// <summary>
/// Get a referencing contents of a content item.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param>
/// <param name="q">The optional json query.</param>
/// <returns>
/// 200 => Content returned.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/{id}/referencing")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetReferencing(string app, string name, DomainId id, [FromQuery] string? q = null)
{
var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id));
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow);
});
return Ok(response);
}
/// <summary> /// <summary>
/// Get a content by version. /// Get a content by version.
/// </summary> /// </summary>

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema { Reference = s }); var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema { Reference = s });
Assert.NotNull(ContentSchemaBuilder.CreateContentSchema(schema, jsonSchema)); Assert.NotNull(jsonSchema);
} }
private static HashSet<string> AllPropertyNames(JsonSchema schema) private static HashSet<string> AllPropertyNames(JsonSchema schema)

26
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs

@ -30,7 +30,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public async Task Should_find_asset_by_slug() public async Task Should_find_asset_by_slug()
{ {
var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), _.RandomValue()); var random = _.RandomValue();
var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), random);
Assert.NotNull(asset); Assert.NotNull(asset);
} }
@ -38,7 +40,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public async Task Should_query_asset_by_hash() public async Task Should_query_asset_by_hash()
{ {
var assets = await _.AssetRepository.FindAssetAsync(_.RandomAppId(), _.RandomValue(), _.RandomValue(), 123); var random = _.RandomValue();
var assets = await _.AssetRepository.FindAssetAsync(_.RandomAppId(), random, random, 1024);
Assert.NotNull(assets); Assert.NotNull(assets);
} }
@ -68,9 +72,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))] [MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_tags(DomainId? parentId) public async Task Should_query_assets_by_tags(DomainId? parentId)
{ {
var random = _.RandomValue();
var query = new ClrQuery var query = new ClrQuery
{ {
Filter = F.Eq("Tags", _.RandomValue()) Filter = F.Eq("Tags", random)
}; };
var assets = await QueryAsync(parentId, query); var assets = await QueryAsync(parentId, query);
@ -82,9 +88,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))] [MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_tags_and_name(DomainId? parentId) public async Task Should_query_assets_by_tags_and_name(DomainId? parentId)
{ {
var random = _.RandomValue();
var query = new ClrQuery var query = new ClrQuery
{ {
Filter = F.And(F.Eq("Tags", _.RandomValue()), F.Contains("FileName", _.RandomValue())) Filter = F.And(F.Eq("Tags", random), F.Contains("FileName", random))
}; };
var assets = await QueryAsync(parentId, query); var assets = await QueryAsync(parentId, query);
@ -96,9 +104,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))] [MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_fileName(DomainId? parentId) public async Task Should_query_assets_by_fileName(DomainId? parentId)
{ {
var random = _.RandomValue();
var query = new ClrQuery var query = new ClrQuery
{ {
Filter = F.Contains("FileName", _.RandomValue()) Filter = F.Contains("FileName", random)
}; };
var assets = await QueryAsync(parentId, query); var assets = await QueryAsync(parentId, query);
@ -110,9 +120,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))] [MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_fileName_and_tags(DomainId? parentId) public async Task Should_query_assets_by_fileName_and_tags(DomainId? parentId)
{ {
var random = _.RandomValue();
var query = new ClrQuery var query = new ClrQuery
{ {
Filter = F.And(F.Contains("FileName", _.RandomValue()), F.Eq("Tags", _.RandomValue())) Filter = F.And(F.Contains("FileName", random), F.Eq("Tags", random))
}; };
var assets = await QueryAsync(parentId, query); var assets = await QueryAsync(parentId, query);
@ -137,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
new SortNode("LastModified", SortOrder.Descending) new SortNode("LastModified", SortOrder.Descending)
}; };
var assets = await _.AssetRepository.QueryAsync(_.RandomAppId(), parentId, query); var assets = await _.AssetRepository.QueryAsync(_.RandomAppId(), parentId, Q.Empty.WithQuery(query));
return assets; return assets;
} }

42
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs

@ -34,16 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
sut = new AssetQueryParser(JsonHelper.DefaultSerializer, tagService, options); sut = new AssetQueryParser(JsonHelper.DefaultSerializer, tagService, options);
} }
[Fact]
public async Task Should_use_existing_query()
{
var clrQuery = new ClrQuery();
var parsed = await sut.ParseQueryAsync(requestContext, Q.Empty.WithQuery(clrQuery));
Assert.Same(parsed, clrQuery);
}
[Fact] [Fact]
public async Task Should_throw_if_odata_query_is_invalid() public async Task Should_throw_if_odata_query_is_invalid()
{ {
@ -65,9 +55,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World");
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending, id Ascending", parsed.ToString()); Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -75,9 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'");
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -85,9 +75,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'fileName', 'op': 'eq', 'value': 'ABC' } }")); var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'fileName', 'op': 'eq', 'value': 'ABC' } }"));
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -95,9 +85,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }"));
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -105,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var query = Q.Empty; var query = Q.Empty;
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -115,9 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); var query = Q.Empty.WithODataQuery("$top=300&$skip=20");
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -125,9 +115,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc");
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", parsed.ToString()); Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString());
} }
[Fact] [Fact]
@ -138,9 +128,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'");
var parsed = await sut.ParseQueryAsync(requestContext, query); var q = await sut.ParseQueryAsync(requestContext, query);
Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
private static string Json(string text) private static string Json(string text)

33
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -12,7 +12,6 @@ using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets.Queries namespace Squidex.Domain.Apps.Entities.Assets.Queries
@ -32,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId));
A.CallTo(() => queryParser.ParseQueryAsync(requestContext, A<Q>._)) A.CallTo(() => queryParser.ParseQueryAsync(requestContext, A<Q>._))
.Returns(new ClrQuery()); .ReturnsLazily(c => new ValueTask<Q>(c.GetArgument<Q>(1)!));
sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser); sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser);
} }
@ -73,30 +72,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
Assert.Same(enriched, result); Assert.Same(enriched, result);
} }
[Fact]
public async Task Should_load_assets_from_ids_and_resolve_tags()
{
var found1 = new AssetEntity { Id = DomainId.NewGuid() };
var found2 = new AssetEntity { Id = DomainId.NewGuid() };
var enriched1 = new AssetEntity();
var enriched2 = new AssetEntity();
var ids = HashSet.Of(found1.Id, found2.Id);
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<HashSet<DomainId>>.That.Is(ids)))
.Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext))
.Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 });
var result = await sut.QueryAsync(requestContext, null, Q.Empty.WithIds(ids));
Assert.Equal(8, result.Total);
Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray());
}
[Fact] [Fact]
public async Task Should_load_assets_with_query_and_resolve_tags() public async Task Should_load_assets_with_query_and_resolve_tags()
{ {
@ -108,13 +83,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
var parentId = DomainId.NewGuid(); var parentId = DomainId.NewGuid();
A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, A<ClrQuery>._)) var q = Q.Empty.WithODataQuery("fileName eq 'Name'");
A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q))
.Returns(ResultList.CreateFrom(8, found1, found2)); .Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext)) A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext))
.Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 }); .Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 });
var result = await sut.QueryAsync(requestContext, parentId, Q.Empty); var result = await sut.QueryAsync(requestContext, parentId, q);
Assert.Equal(8, result.Total); Assert.Equal(8, result.Total);

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs

@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var (id, data, query) = CreateTestData(true); var (id, data, query) = CreateTestData(true);
A.CallTo(() => contentQuery.QueryAsync(requestContext, A<string>._, A<Q>.That.Matches(x => x.ParsedJsonQuery == query))) A.CallTo(() => contentQuery.QueryAsync(requestContext, A<string>._, A<Q>.That.Matches(x => x.JsonQuery == query)))
.Returns(ResultList.CreateFrom(1, CreateContent(id))); .Returns(ResultList.CreateFrom(1, CreateContent(id)));
var command = new BulkUpdateContents var command = new BulkUpdateContents
@ -199,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var (id, data, query) = CreateTestData(true); var (id, data, query) = CreateTestData(true);
A.CallTo(() => contentQuery.QueryAsync(requestContext, A<string>._, A<Q>.That.Matches(x => x.ParsedJsonQuery == query))) A.CallTo(() => contentQuery.QueryAsync(requestContext, A<string>._, A<Q>.That.Matches(x => x.JsonQuery == query)))
.Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id))); .Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id)));
var command = new BulkUpdateContents var command = new BulkUpdateContents

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs

@ -176,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Empty(result); Assert.Empty(result);
A.CallTo(() => contentQuery.QueryAsync(ctx, A<IReadOnlyList<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentIndex.SearchAsync("query~", ctx.App, A<SearchFilter>.That.IsEqualTo(searchFilter), ctx.Scope())) A.CallTo(() => contentIndex.SearchAsync("query~", ctx.App, A<SearchFilter>.That.IsEqualTo(searchFilter), ctx.Scope()))
.Returns(ids); .Returns(ids);
A.CallTo(() => contentQuery.QueryAsync(ctx, ids)) A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>.That.HasIds(ids)))
.Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1, content)); .Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1, content));
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId1, content.Id)) A.CallTo(() => urlGenerator.ContentUI(appId, schemaId1, content.Id))

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

@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", assetId.ToString()); }".Replace("<ID>", assetId.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasIds(assetId)))
.Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(1)); .Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(1));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", assetId.ToString()).Replace("<FIELDS>", TestAsset.AllFields); }".Replace("<ID>", assetId.ToString()).Replace("<FIELDS>", TestAsset.AllFields);
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasIds(assetId)))
.Returns(ResultList.CreateFrom(1, asset)); .Returns(ResultList.CreateFrom(1, asset));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -344,7 +344,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1)); .Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -373,7 +373,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<FIELDS>", TestContent.AllFields).Replace("<ID>", contentId.ToString()); }".Replace("<FIELDS>", TestContent.AllFields).Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -417,10 +417,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>._))
.Returns(ResultList.CreateFrom(0, contentRef)); .Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -483,10 +483,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentRefId.ToString()); }".Replace("<ID>", contentRefId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentRefId)))
.Returns(ResultList.CreateFrom(0, contentRef)); .Returns(ResultList.CreateFrom(1, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5", contentRefId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5", contentRefId)))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -546,10 +546,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentRefId.ToString()); }".Replace("<ID>", contentRefId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentRefId)))
.Returns(ResultList.CreateFrom(0, contentRef)); .Returns(ResultList.CreateFrom(1, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5", contentRefId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5", contentRefId)))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -619,10 +619,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>._))
.Returns(ResultList.CreateFrom(0, contentRef)); .Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -685,7 +685,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>._)) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>._))
@ -741,10 +741,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", assetId2.ToString()); }".Replace("<ID>", assetId2.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId1))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasIds(assetId1)))
.Returns(ResultList.CreateFrom(0, asset1)); .Returns(ResultList.CreateFrom(0, asset1));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId2))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasIds(assetId2)))
.Returns(ResultList.CreateFrom(0, asset2)); .Returns(ResultList.CreateFrom(0, asset2));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 });
@ -800,7 +800,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}".Replace("<ID>", contentId.ToString()); }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -810,16 +810,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
Assert.Contains("\"data\":null", json); Assert.Contains("\"data\":null", json);
} }
private static IReadOnlyList<DomainId> MatchId(DomainId contentId)
{
return A<IReadOnlyList<DomainId>>.That.Matches(x => x.Count == 1 && x[0] == contentId);
}
private static Q MatchIdQuery(DomainId contentId)
{
return A<Q>.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId);
}
private Context MatchsAssetContext() private Context MatchsAssetContext()
{ {
return A<Context>.That.Matches(x => x.App == app && x.User == requestContext.User); return A<Context>.That.Matches(x => x.App == app && x.User == requestContext.User);

27
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Xunit; using Xunit;
@ -43,7 +44,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet();
var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), ids, SearchScope.All); var schemas = new List<ISchemaEntity>
{
_.RandomSchema()
};
var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All);
Assert.NotNull(contents); Assert.NotNull(contents);
} }
@ -53,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet();
var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), ids, SearchScope.All); var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All);
Assert.NotNull(contents); Assert.NotNull(contents);
} }
@ -80,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
Filter = F.Eq("data.value.iv", 12) Filter = F.Eq("data.value.iv", 12)
}; };
var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), query, null, SearchScope.Published); var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithQuery(query), SearchScope.Published);
Assert.NotEmpty(contents); Assert.NotEmpty(contents);
} }
@ -108,9 +114,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{ {
var query = new ClrQuery(); var query = new ClrQuery();
var contents = await QueryAsync(query, id: DomainId.NewGuid()); var contents = await QueryAsync(query, reference: DomainId.NewGuid());
Assert.NotEmpty(contents); Assert.Empty(contents);
} }
[Fact] [Fact]
@ -163,12 +169,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
Filter = F.Eq("data.value.iv", 12) Filter = F.Eq("data.value.iv", 12)
}; };
var contents = await QueryAsync(query, 1000, 0, id: DomainId.NewGuid()); var contents = await QueryAsync(query, 1000, 0, reference: DomainId.NewGuid());
Assert.Empty(contents); Assert.Empty(contents);
} }
private async Task<IResultList<IContentEntity>> QueryAsync(ClrQuery clrQuery, int take = 1000, int skip = 100, DomainId? id = null) private async Task<IResultList<IContentEntity>> QueryAsync(ClrQuery clrQuery, int take = 1000, int skip = 100, DomainId reference = default)
{ {
if (clrQuery.Take == long.MaxValue) if (clrQuery.Take == long.MaxValue)
{ {
@ -188,7 +194,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
}; };
} }
var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), clrQuery, id, SearchScope.All); var q =
Q.Empty
.WithQuery(clrQuery)
.WithReference(reference);
var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All);
return contents; return contents;
} }

66
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs

@ -46,29 +46,39 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
[Fact] [Fact]
public async Task Should_use_existing_query() public async Task Should_throw_if_odata_query_is_invalid()
{ {
var clrQuery = new ClrQuery(); var query = Q.Empty.WithODataQuery("$filter=invalid");
await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query, schema).AsTask());
}
var parsed = await sut.ParseQueryAsync(requestContext, schema, Q.Empty.WithQuery(clrQuery)); [Fact]
public async Task Should_throw_if_json_query_is_invalid()
{
var query = Q.Empty.WithJsonQuery("invalid");
Assert.Same(parsed, clrQuery); await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query, schema).AsTask());
} }
[Fact] [Fact]
public async Task Should_throw_if_odata_query_is_invalid() public async Task Should_parse_odata_query_without_schema()
{ {
var query = Q.Empty.WithODataQuery("$filter=invalid"); var query = Q.Empty.WithODataQuery("$filter=status eq 'Draft'");
var q = await sut.ParseAsync(requestContext, query);
await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask()); Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
public async Task Should_throw_if_json_query_is_invalid() public async Task Should_parse_json_query_without_schema()
{ {
var query = Q.Empty.WithJsonQuery("invalid"); var query = Q.Empty.WithJsonQuery("{ 'filter': { 'path': 'status', 'op': 'eq', 'value': 'Draft' } }");
var q = await sut.ParseAsync(requestContext, query);
await Assert.ThrowsAsync<ValidationException>(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask()); Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -76,9 +86,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$search=Hello World"); var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$search=Hello World");
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", parsed.ToString()); Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -86,9 +96,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'"); var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'");
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -96,9 +106,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'data.firstName.iv', 'op': 'eq', 'value': 'ABC' } }")); var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'data.firstName.iv', 'op': 'eq', 'value': 'ABC' } }"));
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -110,9 +120,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Filter = new CompareFilter<IJsonValue>("data.firstName.iv", CompareOperator.Equals, JsonValue.Create("ABC")) Filter = new CompareFilter<IJsonValue>("data.firstName.iv", CompareOperator.Equals, JsonValue.Create("ABC"))
}); });
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -120,9 +130,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }"));
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -134,9 +144,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
FullText = "Hello" FullText = "Hello"
}); });
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -144,9 +154,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var query = Q.Empty; var query = Q.Empty;
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -154,9 +164,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); var query = Q.Empty.WithODataQuery("$top=300&$skip=20");
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString());
} }
[Fact] [Fact]
@ -164,9 +174,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc");
var parsed = await sut.ParseQueryAsync(requestContext, schema, query); var q = await sut.ParseAsync(requestContext, query, schema);
Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", parsed.ToString()); Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString());
} }
private static string Json(string text) private static string Json(string text)

49
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
@ -53,8 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A<bool>._)) A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A<bool>._))
.Returns(schema); .Returns(schema);
A.CallTo(() => queryParser.ParseQueryAsync(A<Context>._, schema, A<Q>._)) A.CallTo(() => appProvider.GetSchemasAsync(appId.Id))
.Returns(new ClrQuery()); .Returns(new List<ISchemaEntity> { schema });
A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<ISchemaEntity?>._))
.ReturnsLazily(c => new ValueTask<Q>(c.GetArgument<Q>(1)!));
sut = new ContentQueryService( sut = new ContentQueryService(
appProvider, appProvider,
@ -180,18 +182,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[InlineData(0, 0, SearchScope.Published)] [InlineData(0, 0, SearchScope.Published)]
public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope) public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope)
{ {
var reference = DomainId.NewGuid();
var ctx = var ctx =
CreateContext(isFrontend: isFrontend == 1, allowSchema: true) CreateContext(isFrontend: isFrontend == 1, allowSchema: true)
.WithUnpublished(unpublished == 1); .WithUnpublished(unpublished == 1);
var content = CreateContent(contentId); var content = CreateContent(contentId);
A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A<ClrQuery>._, reference, scope)) var q = Q.Empty.WithReference(DomainId.NewGuid());
A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, q, scope))
.Returns(ResultList.CreateFrom(5, content)); .Returns(ResultList.CreateFrom(5, content));
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithReference(reference)); var result = await sut.QueryAsync(ctx, schemaId.Name, q);
Assert.Equal(contentData, result[0].Data); Assert.Equal(contentData, result[0].Data);
Assert.Equal(contentId, result[0].Id); Assert.Equal(contentId, result[0].Id);
@ -200,16 +202,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
[Fact] [Fact]
public async Task QueryByIds_should_not_return_contents_if_user_has_no_permission() public async Task QueryAll_should_not_return_contents_if_user_has_no_permission()
{ {
var ctx = CreateContext(isFrontend: false, allowSchema: false); var ctx = CreateContext(isFrontend: false, allowSchema: false);
var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList();
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<HashSet<DomainId>>._, SearchScope.All)) var q = Q.Empty.WithIds(ids);
.Returns(ids.Select(x => (CreateContent(x), schema)).ToList());
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<List<ISchemaEntity>>.That.Matches(x => x.Count == 0), q, SearchScope.All))
.Returns(ResultList.Create(0, ids.Select(CreateContent)));
var result = await sut.QueryAsync(ctx, ids); var result = await sut.QueryAsync(ctx, q);
Assert.Empty(result); Assert.Empty(result);
} }
@ -219,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[InlineData(1, 1, SearchScope.All)] [InlineData(1, 1, SearchScope.All)]
[InlineData(0, 1, SearchScope.All)] [InlineData(0, 1, SearchScope.All)]
[InlineData(0, 0, SearchScope.Published)] [InlineData(0, 0, SearchScope.Published)]
public async Task QueryByIds_should_return_contents(int isFrontend, int unpublished, SearchScope scope) public async Task QueryAll_should_return_contents(int isFrontend, int unpublished, SearchScope scope)
{ {
var ctx = var ctx =
CreateContext(isFrontend: isFrontend == 1, allowSchema: true) CreateContext(isFrontend: isFrontend == 1, allowSchema: true)
@ -227,25 +231,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList();
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<HashSet<DomainId>>._, scope)) var q = Q.Empty.WithIds(ids);
.Returns(ids.Select(x => (CreateContent(x), schema)).ToList());
var result = await sut.QueryAsync(ctx, ids);
Assert.Equal(ids, result.Select(x => x.Id).ToList());
}
[Fact]
public async Task QueryByIds_should_not_call_repository_if_no_id_defined()
{
var ctx = CreateContext(isFrontend: false, allowSchema: true);
var result = await sut.QueryAsync(ctx, new List<DomainId>()); A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<List<ISchemaEntity>>.That.Matches(x => x.Count == 1), q, scope))
.Returns(ResultList.Create(5, ids.Select(CreateContent)));
Assert.Empty(result); var result = await sut.QueryAsync(ctx, q);
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<HashSet<DomainId>>._, A<SearchScope>._)) Assert.Equal(ids, result.Select(x => x.Id).ToList());
.MustNotHaveHappened();
} }
private void SetupEnricher() private void SetupEnricher()

50
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs

@ -75,52 +75,52 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_add_assets_id_and_versions_as_dependency() public async Task Should_add_assets_id_and_versions_as_dependency()
{ {
var document1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx"); var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx");
var document2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx"); var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx");
var contents = new[] var contents = new[]
{ {
CreateContent( CreateContent(
new[] { document1.Id }, new[] { doc1.Id },
new[] { document1.Id }), new[] { doc1.Id }),
CreateContent( CreateContent(
new[] { document2.Id }, new[] { doc2.Id },
new[] { document2.Id }) new[] { doc2.Id })
}; };
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.Matches(x => x.Ids.Count == 2))) A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.HasIds(doc1.Id, doc2.Id)))
.Returns(ResultList.CreateFrom(4, document1, document2)); .Returns(ResultList.CreateFrom(4, doc1, doc2));
await sut.EnrichAsync(requestContext, contents, schemaProvider); await sut.EnrichAsync(requestContext, contents, schemaProvider);
A.CallTo(() => requestCache.AddDependency(document1.UniqueId, document1.Version)) A.CallTo(() => requestCache.AddDependency(doc1.UniqueId, doc1.Version))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => requestCache.AddDependency(document2.UniqueId, document2.Version)) A.CallTo(() => requestCache.AddDependency(doc2.UniqueId, doc2.Version))
.MustHaveHappened(); .MustHaveHappened();
} }
[Fact] [Fact]
public async Task Should_enrich_with_asset_urls() public async Task Should_enrich_with_asset_urls()
{ {
var image1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png"); var img1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png");
var image2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Image, "Image2.png"); var img2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Image, "Image2.png");
var document1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png"); var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png");
var document2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png"); var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png");
var contents = new[] var contents = new[]
{ {
CreateContent( CreateContent(
new[] { image1.Id }, new[] { img1.Id },
new[] { image2.Id, image1.Id }), new[] { img2.Id, img1.Id }),
CreateContent( CreateContent(
new[] { document1.Id }, new[] { doc1.Id },
new[] { document2.Id, document1.Id }) new[] { doc2.Id, doc1.Id })
}; };
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.Matches(x => x.Ids.Count == 4))) A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.HasIds(doc1.Id, doc2.Id, img1.Id, img2.Id)))
.Returns(ResultList.CreateFrom(4, image1, image2, document1, document2)); .Returns(ResultList.CreateFrom(4, img1, img2, doc1, doc2));
await sut.EnrichAsync(requestContext, contents, schemaProvider); await sut.EnrichAsync(requestContext, contents, schemaProvider);
@ -128,20 +128,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new NamedContentData() new NamedContentData()
.AddField("asset1", .AddField("asset1",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array($"url/to/{image1.Id}", image1.FileName))) .AddValue("iv", JsonValue.Array($"url/to/{img1.Id}", img1.FileName)))
.AddField("asset2", .AddField("asset2",
new ContentFieldData() new ContentFieldData()
.AddValue("en", JsonValue.Array($"url/to/{image2.Id}", image2.FileName))), .AddValue("en", JsonValue.Array($"url/to/{img2.Id}", img2.FileName))),
contents[0].ReferenceData); contents[0].ReferenceData);
Assert.Equal( Assert.Equal(
new NamedContentData() new NamedContentData()
.AddField("asset1", .AddField("asset1",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array(document1.FileName))) .AddValue("iv", JsonValue.Array(doc1.FileName)))
.AddField("asset2", .AddField("asset2",
new ContentFieldData() new ContentFieldData()
.AddValue("en", JsonValue.Array(document2.FileName))), .AddValue("en", JsonValue.Array(doc2.FileName))),
contents[1].ReferenceData); contents[1].ReferenceData);
} }
@ -212,7 +212,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.NotNull(contents[0].ReferenceData); Assert.NotNull(contents[0].ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.Matches(x => x.Ids.Count == 1))) A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.HasIds(id1)))
.MustHaveHappened(); .MustHaveHappened();
} }

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs

@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id })
}; };
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<IReadOnlyList<DomainId>>.That.Matches(x => x.Count == 4))) A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id)))
.Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2));
await sut.EnrichAsync(requestContext, contents, schemaProvider); await sut.EnrichAsync(requestContext, contents, schemaProvider);
@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id })
}; };
A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichContent()), A<IReadOnlyList<DomainId>>.That.Matches(x => x.Count == 4))) A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichContent()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id)))
.Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2));
await sut.EnrichAsync(requestContext, contents, schemaProvider); await sut.EnrichAsync(requestContext, contents, schemaProvider);
@ -192,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id }) CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id })
}; };
A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichContent()), A<IReadOnlyList<DomainId>>.That.Matches(x => x.Count == 4))) A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichContent()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id)))
.Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2));
await sut.EnrichAsync(requestContext, contents, schemaProvider); await sut.EnrichAsync(requestContext, contents, schemaProvider);
@ -244,7 +244,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Null(contents[0].ReferenceData); Assert.Null(contents[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<List<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -262,7 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Null(contents[0].ReferenceData); Assert.Null(contents[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<List<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -278,7 +278,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.NotNull(contents[0].ReferenceData); Assert.NotNull(contents[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<List<DomainId>>._)) A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs

@ -56,10 +56,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
AppId = appId AppId = appId
}; };
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<IReadOnlyList<DomainId>>.That.Contains(referenceId1))) A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId1)))
.Returns(ResultList.CreateFrom(1, reference1)); .Returns(ResultList.CreateFrom(1, reference1));
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<IReadOnlyList<DomainId>>.That.Contains(referenceId2))) A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId2)))
.Returns(ResultList.CreateFrom(1, reference2)); .Returns(ResultList.CreateFrom(1, reference2));
var vars = new TemplateVars var vars = new TemplateVars

14
backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs

@ -22,14 +22,24 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
public static Q HasOData(this INegatableArgumentConstraintManager<Q> that, string odata) public static Q HasOData(this INegatableArgumentConstraintManager<Q> that, string odata)
{ {
return that.HasOData(odata, null); return that.HasOData(odata, default);
} }
public static Q HasOData(this INegatableArgumentConstraintManager<Q> that, string odata, DomainId? reference = null) public static Q HasOData(this INegatableArgumentConstraintManager<Q> that, string odata, DomainId reference = default)
{ {
return that.Matches(x => x.ODataQuery == odata && x.Reference == reference); return that.Matches(x => x.ODataQuery == odata && x.Reference == reference);
} }
public static Q HasIds(this INegatableArgumentConstraintManager<Q> that, params DomainId[] ids)
{
return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids));
}
public static Q HasIds(this INegatableArgumentConstraintManager<Q> that, IEnumerable<DomainId> ids)
{
return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids.ToHashSet()));
}
public static ClrQuery Is(this INegatableArgumentConstraintManager<ClrQuery> that, string query) public static ClrQuery Is(this INegatableArgumentConstraintManager<ClrQuery> that, string query)
{ {
return that.Matches(x => x.ToString() == query); return that.Matches(x => x.ToString() == query);

Loading…
Cancel
Save