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. 30
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 40
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs
  3. 110
      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. 115
      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. 43
      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. 115
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  25. 103
      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. 69
      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

30
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.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Translations;
using Squidex.Log;
@ -82,13 +81,25 @@ 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"))
{
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);
@ -104,6 +115,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return ResultList.Create<IAssetEntity>(total, items);
}
}
catch (MongoQueryException ex) when (ex.Message.Contains("17406"))
{
throw new DomainException(T.Get("common.resultTooLarge"));
@ -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)
{
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;
}
}
}

110
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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
@ -25,24 +24,29 @@ using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public sealed class MongoContentCollectionAll : MongoRepositoryBase<MongoContentEntity>
{
private readonly QueryContent queryContentAsync;
private readonly QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery;
private readonly QueryIdsAsync queryIdsAsync;
private readonly QueryReferrersAsync queryReferrersAsync;
private readonly QueryScheduledContents queryScheduledItems;
public MongoContentCollectionAll(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter)
public sealed class MongoContentCollection : MongoRepositoryBase<MongoContentEntity>
{
private readonly QueryAsStream queryAsStream;
private readonly QueryById queryBdId;
private readonly QueryByIds queryByIds;
private readonly QueryByQuery queryByQuery;
private readonly QueryReferences queryReferences;
private readonly QueryReferrers queryReferrers;
private readonly QueryScheduled queryScheduled;
private readonly string name;
public MongoContentCollection(string name, IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter dataConverter)
: base(database)
{
queryContentAsync = new QueryContent(converter);
queryContentsById = new QueryContentsByIds(converter, appProvider);
queryContentsByQuery = new QueryContentsByQuery(converter, indexer, appProvider);
queryIdsAsync = new QueryIdsAsync(appProvider);
queryReferrersAsync = new QueryReferrersAsync();
queryScheduledItems = new QueryScheduledContents();
this.name = name;
queryAsStream = new QueryAsStream(dataConverter, appProvider);
queryBdId = new QueryById(dataConverter);
queryByIds = new QueryByIds(dataConverter);
queryByQuery = new QueryByQuery(dataConverter, indexer, appProvider);
queryReferences = new QueryReferences(dataConverter, queryByIds);
queryReferrers = new QueryReferrers(dataConverter);
queryScheduled = new QueryScheduled(dataConverter);
}
public IMongoCollection<MongoContentEntity> GetInternalCollection()
@ -52,53 +56,63 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
protected override string CollectionName()
{
return "States_Contents_All2";
return name;
}
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 queryIdsAsync.PrepareAsync(collection, ct);
await queryReferrersAsync.PrepareAsync(collection, ct);
await queryScheduledItems.PrepareAsync(collection, ct);
await queryAsStream.PrepareAsync(collection, ct);
await queryBdId.PrepareAsync(collection, ct);
await queryByIds.PrepareAsync(collection, ct);
await queryByQuery.PrepareAsync(collection, ct);
await queryReferences.PrepareAsync(collection, ct);
await queryReferrers.PrepareAsync(collection, ct);
await queryScheduled.PrepareAsync(collection, ct);
}
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);
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<DomainId> ids)
if (q.Referencing != default)
{
Guard.NotNull(app, nameof(app));
return await queryReferences.QueryAsync(app.Id, schemas, q);
}
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds"))
if (q.Reference != default)
{
var result = await queryContentsById.DoAsync(app.Id, schema, ids, false);
return await queryByQuery.QueryAsync(app, schemas, q);
}
return ResultList.Create(result.Count, result.Select(x => x.Content));
throw new NotSupportedException();
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<DomainId> ids)
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
if (q.Ids != null && q.Ids.Count > 0l)
{
Guard.NotNull(app, nameof(app));
return await queryByIds.QueryAsync(app.Id, new List<ISchemaEntity> { schema }, q);
}
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>())
{
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>())
{
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>())
{
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>())
{
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>())
{
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)

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 DataConverter converter;
private readonly MongoContentCollectionAll collectionAll;
private readonly MongoContentCollectionPublished collectionPublished;
private readonly MongoContentCollection collectionAll;
private readonly MongoContentCollection collectionPublished;
static MongoContentRepository()
{
@ -45,8 +45,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
converter = new DataConverter(serializer);
collectionAll = new MongoContentCollectionAll(database, appProvider, indexer, converter);
collectionPublished = new MongoContentCollectionPublished(database, appProvider, indexer, converter);
collectionAll =
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)
@ -60,39 +65,27 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionAll.StreamAll(appId, schemaIds);
}
public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, 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)
public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, SearchScope scope)
{
if (scope == SearchScope.All)
{
return collectionAll.QueryAsync(app, schema, ids);
return collectionAll.QueryAsync(app, schemas, q);
}
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)
{
return collectionAll.QueryAsync(app, ids);
return collectionAll.QueryAsync(app, schema, q, scope);
}
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;
}
public static Func<PropertyPath, PropertyPath> Path(Schema schema)
public static Func<PropertyPath, PropertyPath> Path(Schema? schema)
{
return propertyNames =>
{
var result = new List<string>(propertyNames);
if (result.Count > 1)
if (result.Count > 1 && schema != null)
{
var rootEdmName = result[1].UnescapeEdmField();
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);

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 DataConverter DataConverter { get; }
protected OperationBase(DataConverter dataConverter)
{
DataConverter = dataConverter;
}
public Task PrepareAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{
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
{
internal sealed class QueryContent : OperationBase
internal sealed class QueryById : OperationBase
{
private readonly DataConverter converter;
public QueryContent(DataConverter converter)
public QueryById(DataConverter dataConverter)
: base(dataConverter)
{
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));
@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return null;
}
contentEntity?.ParseData(schema.SchemaDef, converter);
contentEntity?.ParseData(schema.SchemaDef, DataConverter);
}
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);
}
}
}

115
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.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes;
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;
@ -23,9 +25,8 @@ using Squidex.Infrastructure.Translations;
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 IAppProvider appProvider;
@ -39,10 +40,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
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.appProvider = appProvider;
}
@ -67,41 +69,89 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
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 =
schemaIds != null ?
Collection.Find(x => x.IndexedAppId == appId && schemaIds.Contains(x.IndexedSchemaId) && !x.IsDeleted) :
Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted);
Guard.NotNull(filterNode, nameof(filterNode));
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 filter = BuildFilter(appId, schemaId, filterNode.AdjustToModel(schema.SchemaDef));
var contentItems = await Collection.FindStatusAsync(filter);
return contentItems.Select(x => (x.IndexedSchemaId, x.Id, x.Status)).ToList();
}
catch (MongoCommandException ex) when (ex.Code == 96)
{
var schema = await appProvider.GetSchemaAsync(appId, entity.SchemaId.Id, false);
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);
if (schema != null)
var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = FindContentsAsync(query, filter);
var (items, total) = await AsyncHelper.WhenAll(contentItems, contentCount);
if (items.Count > 0)
{
entity.ParseData(schema.SchemaDef, converter);
var contentSchemas = schemas.ToDictionary(x => x.Id);
yield return entity;
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(schema, nameof(schema));
Guard.NotNull(query, nameof(query));
Guard.NotNull(q, nameof(q));
try
{
query = query.AdjustToModel(schema.SchemaDef);
var query = q.Query.AdjustToModel(schema.SchemaDef);
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 contentItems = FindContentsAsync(query, filter);
@ -126,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
foreach (var entity in items)
{
entity.ParseData(schema.SchemaDef, converter);
entity.ParseData(schema.SchemaDef, DataConverter);
}
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;
}
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>>
{
@ -189,6 +239,23 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
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)
{
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>());
}
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);

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
{
internal sealed class QueryReferrersAsync : OperationBase
internal sealed class QueryReferrers : OperationBase
{
public QueryReferrers(DataConverter dataConverter)
: base(dataConverter)
{
}
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
@ -26,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
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 =
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
{
internal sealed class QueryScheduledContents : OperationBase
internal sealed class QueryScheduled : OperationBase
{
public QueryScheduled(DataConverter dataConverter)
: base(dataConverter)
{
}
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
@ -28,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
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));

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

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
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);

43
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;
}
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(q, nameof(q));
using (Profiler.TraceMethod<AssetQueryParser>())
{
ClrQuery result;
var query = q.Query;
if (q.Query != null)
if (!string.IsNullOrWhiteSpace(q?.JsonQueryString))
{
result = q.Query;
}
else
{
if (!string.IsNullOrWhiteSpace(q?.JsonQuery))
{
result = ParseJson(q.JsonQuery);
query = ParseJson(q.JsonQueryString);
}
else if (!string.IsNullOrWhiteSpace(q?.ODataQuery))
{
result = ParseOData(q.ODataQuery);
}
else
{
result = new ClrQuery();
}
query = ParseOData(q.ODataQuery);
}
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;
}
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(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)
{
assets = await QueryByIdsAsync(context, query);
}
else
var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q);
if (q.Ids != null && q.Ids.Count > 0)
{
assets = await QueryByQueryAsync(context, parentId, query);
assets = assets.SortSet(x => x.Id, q.Ids);
}
var enriched = await assetEnricher.EnrichAsync(assets, context);
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.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
@ -16,9 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
IAsyncEnumerable<IAssetEntity> StreamAll(DomainId appId);
Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, ClrQuery query);
Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, HashSet<DomainId> ids);
Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, Q q);
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 contents = await contentQuery.QueryAsync(context, ids);
var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids));
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
{
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);

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

@ -35,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public class ContentQueryParser
{
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 IJsonSerializer jsonSerializer;
private readonly ContentOptions options;
@ -50,78 +52,68 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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(schema, nameof(schema));
Guard.NotNull(q, nameof(q));
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))
{
result = ParseJson(context, schema, q.JsonQuery);
}
else if (q?.ParsedJsonQuery != null)
{
result = ParseJson(context, schema, q.ParsedJsonQuery);
query = ParseJson(context, schema, q.JsonQuery);
}
else if (!string.IsNullOrWhiteSpace(q?.ODataQuery))
{
result = ParseOData(context, schema, q.ODataQuery);
}
else
{
result = new ClrQuery();
}
query = ParseOData(context, schema, q.ODataQuery);
}
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);
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);
return jsonSchema.Parse(json, jsonSerializer);
}
private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata)
private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata)
{
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 result = cache.GetOrCreate(cacheKey, entry =>
@ -153,8 +150,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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 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);
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)
@ -207,7 +236,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
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.Status).ToCamelCase(), EdmPrimitiveTypeKind.String);
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");
container.AddEntitySet("ContentSet", entityType);
model.AddElement(container);
model.AddElement(schemaType);
model.AddElement(entityType);
return model;

103
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.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Translations;
using Squidex.Log;
using Squidex.Shared;
#pragma warning disable RECS0147
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public sealed class ContentQueryService : IContentQueryService
@ -62,8 +59,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
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));
if (query == null)
if (q == null)
{
return EmptyContents;
}
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
IResultList<IContentEntity> contents;
q = await queryParser.ParseAsync(context, q, schema);
if (query.Ids != null && query.Ids.Count > 0)
{
contents = await QueryByIdsAsync(context, schema, query);
}
else
var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope());
if (q.Ids != null && q.Ids.Count > 0)
{
contents = await QueryByQueryAsync(context, schema, query);
contents = contents.SortSet(x => x.Id, q.Ids);
}
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));
if (ids == null || ids.Count == 0)
if (q == null)
{
return EmptyContents;
}
var schemas = await GetSchemasAsync(context);
if (schemas.Count == 0)
{
return EmptyContents;
}
using (Profiler.TraceMethod<ContentQueryService>())
{
var contents = await QueryCoreAsync(context, ids);
q = await queryParser.ParseAsync(context, q);
var filtered =
contents
.GroupBy(x => x.Schema.Id)
.Select(g => FilterContents(g, context))
.SelectMany(c => c);
var contents = await contentRepository.QueryAsync(context.App, schemas, q, context.Scope());
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);
}
return schema;
}
private static void CheckPermission(Context context, params ISchemaEntity[] schemas)
{
foreach (var schema in schemas)
{
if (!HasPermission(context, schema))
{
throw new DomainForbiddenException(T.Get("schemas.noPermission"));
}
}
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 group.Select(x => x.Content);
}
else
{
return Enumerable.Empty<IContentEntity>();
}
return schemas.Where(x => HasPermission(context, x)).ToList();
}
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);
}
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)
{
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();
try
{
contents = await contentQuery.QueryAsync(context, notLoadedContents);
contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedContents));
}
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;
}
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);
}

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 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();
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);
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, ClrQuery query, DomainId? referenced, SearchScope scope);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope);
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 =>
{
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);

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

@ -13,75 +13,88 @@ using Squidex.Infrastructure.Queries;
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 Clone(clone => clone.Ids = ids.ToList());
return this with { JsonQuery = query };
}
public Q WithReference(DomainId? reference)
public Q WithReferencing(DomainId id)
{
return Clone(clone => clone.Reference = reference);
return this with { Referencing = id };
}
public Q WithReference(DomainId id)
{
return this with { Reference = id };
}
public Q WithIds(params DomainId[] ids)
{
return this with { Ids = ids?.ToList() };
}
public Q WithIds(IEnumerable<DomainId> ids)
{
return Clone(clone => clone.Ids = ids.ToList());
return this with { Ids = ids?.ToList() };
}
public Q WithIds(string? ids)
{
if (!string.IsNullOrEmpty(ids))
{
return Clone(clone =>
if (string.IsNullOrWhiteSpace(ids))
{
return this with { Ids = null };
}
var idsList = new List<DomainId>();
if (!string.IsNullOrEmpty(ids))
{
foreach (var id in ids.Split(','))
{
idsList.Add(DomainId.Create(id));
}
clone.Ids = idsList;
});
}
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
{

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

@ -154,7 +154,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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(() =>
{
@ -183,7 +183,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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(() =>
{
@ -273,7 +273,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ProducesResponseType(typeof(ContentDto), 200)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, DomainId id)
@ -285,6 +285,68 @@ namespace Squidex.Areas.Api.Controllers.Contents
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>
/// Get a content by version.
/// </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 });
Assert.NotNull(ContentSchemaBuilder.CreateContentSchema(schema, jsonSchema));
Assert.NotNull(jsonSchema);
}
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]
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);
}
@ -38,7 +40,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact]
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);
}
@ -68,9 +72,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_tags(DomainId? parentId)
{
var random = _.RandomValue();
var query = new ClrQuery
{
Filter = F.Eq("Tags", _.RandomValue())
Filter = F.Eq("Tags", random)
};
var assets = await QueryAsync(parentId, query);
@ -82,9 +88,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_tags_and_name(DomainId? parentId)
{
var random = _.RandomValue();
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);
@ -96,9 +104,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_fileName(DomainId? parentId)
{
var random = _.RandomValue();
var query = new ClrQuery
{
Filter = F.Contains("FileName", _.RandomValue())
Filter = F.Contains("FileName", random)
};
var assets = await QueryAsync(parentId, query);
@ -110,9 +120,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[MemberData(nameof(ParentIds))]
public async Task Should_query_assets_by_fileName_and_tags(DomainId? parentId)
{
var random = _.RandomValue();
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);
@ -137,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
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;
}

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);
}
[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]
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 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]
@ -75,9 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
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]
@ -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 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]
@ -95,9 +85,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
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]
@ -105,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
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]
@ -115,9 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
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]
@ -125,9 +115,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
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]
@ -138,9 +128,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
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)

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.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Xunit;
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));
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);
}
@ -73,30 +72,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
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]
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();
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));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext))
.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);

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);
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)));
var command = new BulkUpdateContents
@ -199,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
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)));
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);
A.CallTo(() => contentQuery.QueryAsync(ctx, A<IReadOnlyList<DomainId>>._))
A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>._))
.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()))
.Returns(ids);
A.CallTo(() => contentQuery.QueryAsync(ctx, ids))
A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>.That.HasIds(ids)))
.Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1, content));
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());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId)))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasIds(assetId)))
.Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(1));
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);
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId)))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasIds(assetId)))
.Returns(ResultList.CreateFrom(1, asset));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>._))
.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));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._))
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentRefId)))
.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));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._))
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentRefId)))
.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));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<DomainId>>._))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>._))
.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));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>._))
@ -741,10 +741,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".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));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId2)))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasIds(assetId2)))
.Returns(ResultList.CreateFrom(0, asset2));
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());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.HasIds(contentId)))
.Returns(ResultList.CreateFrom(1, content));
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);
}
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()
{
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.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Xunit;
@ -43,7 +44,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
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);
}
@ -53,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
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);
}
@ -80,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
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);
}
@ -108,9 +114,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
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]
@ -163,12 +169,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
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);
}
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)
{
@ -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;
}

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

@ -46,29 +46,39 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
[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]
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]
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]
@ -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 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]
@ -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 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]
@ -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 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]
@ -110,9 +120,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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]
@ -120,9 +130,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
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]
@ -134,9 +144,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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]
@ -144,9 +154,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
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]
@ -154,9 +164,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
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]
@ -164,9 +174,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
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)

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.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
@ -53,8 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A<bool>._))
.Returns(schema);
A.CallTo(() => queryParser.ParseQueryAsync(A<Context>._, schema, A<Q>._))
.Returns(new ClrQuery());
A.CallTo(() => appProvider.GetSchemasAsync(appId.Id))
.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(
appProvider,
@ -180,18 +182,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[InlineData(0, 0, SearchScope.Published)]
public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope)
{
var reference = DomainId.NewGuid();
var ctx =
CreateContext(isFrontend: isFrontend == 1, allowSchema: true)
.WithUnpublished(unpublished == 1);
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));
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(contentId, result[0].Id);
@ -200,16 +202,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
[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 ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList();
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<HashSet<DomainId>>._, SearchScope.All))
.Returns(ids.Select(x => (CreateContent(x), schema)).ToList());
var q = Q.Empty.WithIds(ids);
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);
}
@ -219,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[InlineData(1, 1, SearchScope.All)]
[InlineData(0, 1, SearchScope.All)]
[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 =
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();
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<HashSet<DomainId>>._, scope))
.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 q = Q.Empty.WithIds(ids);
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>._))
.MustNotHaveHappened();
Assert.Equal(ids, result.Select(x => x.Id).ToList());
}
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]
public async Task Should_add_assets_id_and_versions_as_dependency()
{
var document1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx");
var document2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx");
var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx");
var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx");
var contents = new[]
{
CreateContent(
new[] { document1.Id },
new[] { document1.Id }),
new[] { doc1.Id },
new[] { doc1.Id }),
CreateContent(
new[] { document2.Id },
new[] { document2.Id })
new[] { doc2.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)))
.Returns(ResultList.CreateFrom(4, document1, document2));
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.HasIds(doc1.Id, doc2.Id)))
.Returns(ResultList.CreateFrom(4, doc1, doc2));
await sut.EnrichAsync(requestContext, contents, schemaProvider);
A.CallTo(() => requestCache.AddDependency(document1.UniqueId, document1.Version))
A.CallTo(() => requestCache.AddDependency(doc1.UniqueId, doc1.Version))
.MustHaveHappened();
A.CallTo(() => requestCache.AddDependency(document2.UniqueId, document2.Version))
A.CallTo(() => requestCache.AddDependency(doc2.UniqueId, doc2.Version))
.MustHaveHappened();
}
[Fact]
public async Task Should_enrich_with_asset_urls()
{
var image1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png");
var image2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Image, "Image2.png");
var img1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png");
var img2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Image, "Image2.png");
var document1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png");
var document2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png");
var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png");
var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png");
var contents = new[]
{
CreateContent(
new[] { image1.Id },
new[] { image2.Id, image1.Id }),
new[] { img1.Id },
new[] { img2.Id, img1.Id }),
CreateContent(
new[] { document1.Id },
new[] { document2.Id, document1.Id })
new[] { doc1.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)))
.Returns(ResultList.CreateFrom(4, image1, image2, document1, document2));
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, img1, img2, doc1, doc2));
await sut.EnrichAsync(requestContext, contents, schemaProvider);
@ -128,20 +128,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new NamedContentData()
.AddField("asset1",
new ContentFieldData()
.AddValue("iv", JsonValue.Array($"url/to/{image1.Id}", image1.FileName)))
.AddValue("iv", JsonValue.Array($"url/to/{img1.Id}", img1.FileName)))
.AddField("asset2",
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);
Assert.Equal(
new NamedContentData()
.AddField("asset1",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(document1.FileName)))
.AddValue("iv", JsonValue.Array(doc1.FileName)))
.AddField("asset2",
new ContentFieldData()
.AddValue("en", JsonValue.Array(document2.FileName))),
.AddValue("en", JsonValue.Array(doc2.FileName))),
contents[1].ReferenceData);
}
@ -212,7 +212,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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();
}

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 })
};
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));
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 })
};
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));
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 })
};
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));
await sut.EnrichAsync(requestContext, contents, schemaProvider);
@ -244,7 +244,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Null(contents[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<List<DomainId>>._))
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._))
.MustNotHaveHappened();
}
@ -262,7 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Null(contents[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<List<DomainId>>._))
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._))
.MustNotHaveHappened();
}
@ -278,7 +278,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.NotNull(contents[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<List<DomainId>>._))
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._))
.MustNotHaveHappened();
}

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

@ -56,10 +56,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
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));
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));
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)
{
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);
}
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)
{
return that.Matches(x => x.ToString() == query);

Loading…
Cancel
Save