Browse Source

Squidex project updated

pull/206/head
Sebastian Stehle 9 years ago
parent
commit
a7cd21473a
  1. 36
      src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs
  2. 9
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  3. 3
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs
  4. 21
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  5. 172
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  6. 58
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  7. 1
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
  8. 21
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs
  9. 30
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs
  10. 101
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs
  11. 141
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  12. 5
      src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs
  13. 17
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs
  14. 26
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs
  15. 17
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs
  16. 15
      src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs
  17. 8
      src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs
  18. 14
      src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs
  19. 14
      src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs
  20. 158
      src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  21. 24
      src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs
  22. 101
      src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  23. 139
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  24. 11
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  25. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  26. 74
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  27. 2
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  28. 2
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  29. 49
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  30. 4
      src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
  31. 10
      src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  32. 4
      src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs
  33. 4
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs
  34. 2
      src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs
  35. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  36. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  37. 4
      src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs
  38. 23
      src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
  39. 4
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  40. 2
      src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs
  41. 9
      src/Squidex.Infrastructure/Commands/DomainObjectBase.cs
  42. 2
      src/Squidex.Infrastructure/Commands/IDomainObject.cs
  43. 2
      src/Squidex.Infrastructure/States/IPersistence.cs
  44. 13
      src/Squidex.Infrastructure/States/Persistence.cs
  45. 7
      src/Squidex/Areas/Api/Controllers/ApiController.cs
  46. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  47. 4
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  48. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  49. 6
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  50. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  51. 8
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  52. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs
  53. 4
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs
  54. 4
      src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs
  55. 21
      src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs
  56. 4
      src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs
  57. 2
      src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs
  58. 2
      src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  59. 6
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  60. 4
      src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs
  61. 2
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs
  62. 8
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  63. 4
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs
  64. 2
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  65. 12
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  66. 4
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  67. 2
      src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  68. 2
      src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs
  69. 36
      src/Squidex/Config/Domain/ReadServices.cs
  70. 66
      src/Squidex/Config/Domain/StoreServices.cs
  71. 31
      src/Squidex/Config/Domain/WriteServices.cs
  72. 2
      src/Squidex/Config/MyUsageOptions.cs
  73. 2
      src/Squidex/Pipeline/ApiCostsFilter.cs
  74. 4
      src/Squidex/Pipeline/AppApiFilter.cs
  75. 2
      src/Squidex/Pipeline/AppPermissionAttribute.cs
  76. 2
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
  77. 2
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
  78. 10
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
  79. 10
      src/Squidex/Pipeline/GraphQLUrlGenerator.cs
  80. 2
      src/Squidex/Pipeline/IAppFeature.cs
  81. 7
      src/Squidex/Squidex.csproj
  82. 244
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs
  83. 269
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  84. 70
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs
  85. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  86. 10
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs
  87. 99
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs
  88. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs
  89. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs
  90. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs
  91. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs
  92. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  93. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  94. 28
      tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs
  95. 61
      tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs
  96. 11
      tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs
  97. 4
      tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs
  98. 42
      tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs
  99. 48
      tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs

36
src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -29,6 +30,33 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds));
}
public async Task<Guid> FindAppIdByNameAsync(string name)
{
var appEntity =
await Collection.Find(x => x.State.Name == name).Only(x => x.Id)
.FirstOrDefaultAsync();
return appEntity != null ? Guid.Parse(appEntity.Id) : Guid.Empty;
}
public async Task<IReadOnlyList<Guid>> QueryUserAppIdsAsync(string userId)
{
var appEntities =
await Collection.Find(x => x.UserIds.Contains(userId)).Only(x => x.Id)
.ToListAsync();
return appEntities.Select(x => Guid.Parse(x.Id)).ToList();
}
public async Task<IReadOnlyList<string>> QueryUserAppNamesAsync(string userId)
{
var appEntities =
await Collection.Find(x => x.UserIds.Contains(userId)).Project<MongoAppEntity>(Projection.Include(x => x.Id))
.ToListAsync();
return appEntities.Select(x => x.Id).ToList();
}
public async Task<(AppState Value, long Version)> ReadAsync(string key)
{
var existing =
@ -43,14 +71,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
return (null, -1);
}
public async Task<IReadOnlyList<string>> QueryUserAppNamesAsync(string userId)
{
var appEntities =
await Collection.Find(x => x.UserIds.Contains(userId)).Project<MongoAppEntity>(Projection.Include(x => x.Id)).ToListAsync();
return appEntities.Select(x => x.Id).ToList();
}
public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion)
{
try

9
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -27,6 +27,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
}
protected override string CollectionName()
{
return "Snapshots_Assets";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetEntity> collection)
{
return collection.Indexes.CreateOneAsync(
@ -122,8 +127,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingVersion =
await Collection.Find(x => x.Id == key)
.Project<MongoAssetEntity>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync();
await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version)
.FirstOrDefaultAsync();
if (existingVersion != null)
{

3
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs

@ -14,11 +14,12 @@ using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public partial class MongoAssetStatsRepository : MongoRepositoryBase<MongoAssetStatsEntity>, IAssetStatsRepository, IAssetEventConsumer
public partial class MongoAssetStatsRepository : MongoRepositoryBase<MongoAssetStatsEntity>, IAssetStatsRepository, IEventConsumer
{
public MongoAssetStatsRepository(IMongoDatabase database)
: base(database)

21
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -25,13 +25,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
public string DocumentId { get; set; }
[BsonRequired]
[BsonElement("st")]
[BsonRepresentation(BsonType.String)]
public Status Status { get; set; }
[BsonElement]
public Guid Id { get; set; }
[BsonRequired]
[BsonElement("ct")]
@ -73,10 +71,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonElement("rd")]
public List<Guid> ReferencedIdsDeleted { get; set; } = new List<Guid>();
[BsonRequired]
[BsonElement("lt")]
public bool IsLatest { get; set; }
[BsonRequired]
[BsonElement("st")]
[BsonRepresentation(BsonType.String)]
public Status Status { get; set; }
[BsonRequired]
[BsonElement("do")]
[BsonJson]
public IdContentData IdData { get; set; }
public IdContentData DataByIds { get; set; }
NamedContentData IContentEntity.Data
{
@ -85,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public void ParseData(Schema schema)
{
data = IdData.ToData(schema, ReferencedIdsDeleted);
data = DataByIds.ToData(schema, ReferencedIdsDeleted);
}
}
}

172
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -11,74 +11,129 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public class MongoContentRepository : IContentRepository
public partial class MongoContentRepository : MongoRepositoryBase<MongoContentEntity>,
IEventConsumer,
IContentRepository,
ISnapshotStore<ContentState>
{
private const string Prefix = "Projections_Content_";
private readonly IMongoDatabase database;
private readonly IAppProvider appProvider;
protected static FilterDefinitionBuilder<MongoContentEntity> Filter
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider)
: base(database)
{
get
{
return Builders<MongoContentEntity>.Filter;
}
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
}
protected static UpdateDefinitionBuilder<MongoContentEntity> Update
protected override string CollectionName()
{
get
{
return Builders<MongoContentEntity>.Update;
}
return "Snapshots_Assets";
}
protected static ProjectionDefinitionBuilder<MongoContentEntity> Projection
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection)
{
get
{
return Builders<MongoContentEntity>.Projection;
}
await collection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.Id)
.Descending(x => x.Version));
await collection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.SchemaId)
.Descending(x => x.IsLatest)
.Descending(x => x.LastModified));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Status));
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText));
}
protected static IndexKeysDefinitionBuilder<MongoContentEntity> Index
public async Task WriteAsync(string key, ContentState value, long oldVersion, long newVersion)
{
get
var documentId = $"{key}_{oldVersion}";
var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId);
if (schema == null)
{
return Builders<MongoContentEntity>.IndexKeys;
throw new InvalidOperationException($"Cannot find schema {value.SchemaId}");
}
var idData = value.Data?.ToIdModel(schema.SchemaDef, true);
var document = SimpleMapper.Map(value, new MongoContentEntity
{
DocumentId = documentId,
DataText = idData?.ToFullText(),
DataByIds = idData,
ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef),
});
try
{
await Collection.InsertOneAsync(document);
}
catch (MongoWriteException ex)
{
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingVersion =
await Collection.Find(x => x.Id == value.Id && x.IsLatest).Only(x => x.Id, x => x.Version)
.FirstOrDefaultAsync();
if (existingVersion != null)
{
throw new InconsistentStateException(existingVersion.Version, oldVersion, ex);
}
}
else
{
throw;
}
}
}
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider)
public async Task<(ContentState Value, long Version)> ReadAsync(string key)
{
Guard.NotNull(database, nameof(database));
Guard.NotNull(appProvider, nameof(appProvider));
var id = Guid.Parse(key);
this.database = database;
this.appProvider = appProvider;
var existing =
await Collection.Find(x => x.Id == id && x.IsLatest)
.FirstOrDefaultAsync();
if (existing != null)
{
return (SimpleMapper.Map(existing, new ContentState()), existing.Version);
}
return (null, -1);
}
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery)
{
var collection = GetCollection(app.Id);
IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try
{
cursor =
collection
Collection
.Find(odataQuery, schema.Id, schema.SchemaDef, status)
.Take(odataQuery)
.Skip(odataQuery)
@ -103,14 +158,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return contentEntities;
}
public Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery)
public async Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery)
{
var collection = GetCollection(app.Id);
IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try
{
cursor = collection.Find(odataQuery, schema.Id, schema.SchemaDef, status);
cursor = Collection.Find(odataQuery, schema.Id, schema.SchemaDef, status);
}
catch (NotSupportedException)
{
@ -121,15 +174,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
throw new ValidationException("This odata operation is not supported.");
}
return cursor.CountAsync();
return await cursor.CountAsync();
}
public async Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids)
{
var collection = GetCollection(app.Id);
var contentsCount =
await collection.Find(x => ids.Contains(x.Id))
await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest)
.CountAsync();
return contentsCount;
@ -137,10 +188,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids)
{
var collection = GetCollection(app.Id);
var contentEntities =
await collection.Find(x => ids.Contains(x.Id))
await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest)
.ToListAsync();
foreach (var entity in contentEntities)
@ -153,21 +202,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds)
{
var collection = GetCollection(appId);
var contentEntities =
await collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Project<BsonDocument>(Projection.Include(x => x.Id))
await Collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Only(x => x.Id)
.ToListAsync();
return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList();
return contentIds.Except(contentEntities.Select(x => x.Id)).ToList();
}
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id)
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version)
{
var collection = GetCollection(app.Id);
var contentEntity =
await collection.Find(x => x.Id == id)
await Collection.Find(x => x.Id == id && x.Version == version)
.FirstOrDefaultAsync();
contentEntity?.ParseData(schema.SchemaDef);
@ -175,32 +220,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return contentEntity;
}
private async Task ForSchemaAsync(NamedId<Guid> appId, Guid schemaId, Func<IMongoCollection<MongoContentEntity>, ISchemaEntity, Task> action)
{
var collection = GetCollection(appId.Id);
var schema = await appProvider.GetSchemaAsync(appId.Name, schemaId, true);
if (schema == null)
{
return;
}
await action(collection, schema);
}
private Task ForAppIdAsync(Guid appId, Func<IMongoCollection<MongoContentEntity>, Task> action)
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id)
{
var collection = GetCollection(appId);
return action(collection);
}
var contentEntity =
await Collection.Find(x => x.Id == id && x.IsLatest)
.FirstOrDefaultAsync();
private IMongoCollection<MongoContentEntity> GetCollection(Guid appId)
{
var name = $"{Prefix}{appId}";
contentEntity?.ParseData(schema.SchemaDef);
return database.GetCollection<MongoContentEntity>(name);
return contentEntity;
}
}
}

58
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -0,0 +1,58 @@
// ==========================================================================
// MongoContentRepository_EventHandling.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public partial class MongoContentRepository
{
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^(content-)|(asset-)"; }
}
public override Task ClearAsync()
{
return TaskHelper.Done;
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected Task On(AssetDeleted @event)
{
return Collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, @event.AssetId),
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId));
}
protected Task On(ContentDeleted @event)
{
return Collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, @event.ContentId),
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId));
}
}
}

1
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs

@ -69,6 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.SchemaId, schemaId),
Filter.Eq(x => x.IsLatest, true),
Filter.In(x => x.Status, status)
};

21
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs

@ -25,9 +25,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
{
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoRuleEntity> collection)
protected override string CollectionName()
{
return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId));
return "States_Rules";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoRuleEntity> collection)
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.IsDeleted));
}
public async Task<(RuleState Value, long Version)> ReadAsync(string key)
@ -44,12 +50,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
return (null, -1);
}
public async Task<IReadOnlyList<string>> QueryRuleIdsAsync(Guid appId)
public async Task<IReadOnlyList<Guid>> QueryRuleIdsAsync(Guid appId)
{
var ruleEntities =
await Collection.Find(x => x.State.AppId == appId).Project<MongoRuleEntity>(Projection.Include(x => x.Id)).ToListAsync();
await Collection.Find(x => x.State.AppId == appId && !x.State.IsDeleted).Only(x => x.Id)
.ToListAsync();
return ruleEntities.Select(x => x.Id).ToList();
return ruleEntities.Select(x => Guid.Parse(x.Id)).ToList();
}
public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion)
@ -67,8 +74,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingVersion =
await Collection.Find(x => x.Id == key)
.Project<MongoRuleEntity>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync();
await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version)
.FirstOrDefaultAsync();
if (existingVersion != null)
{

30
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs

@ -0,0 +1,30 @@
// ==========================================================================
// MongoSchemaEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Entities.Schemas.State;
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
{
public sealed class MongoSchemaEntity
{
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
[BsonElement]
[BsonRequired]
public SchemaState State { get; set; }
[BsonElement]
[BsonRequired]
public int Version { get; set; }
}
}

101
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs

@ -0,0 +1,101 @@
// ==========================================================================
// MongoSchemaRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Domain.Apps.Entities.Schemas.State;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
{
public sealed class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, ISnapshotStore<SchemaState>
{
public MongoSchemaRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "Snapshots_Schemas";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoSchemaEntity> collection)
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.Name));
}
public async Task<(SchemaState Value, long Version)> ReadAsync(string key)
{
var existing =
await Collection.Find(x => x.Id == key)
.FirstOrDefaultAsync();
if (existing != null)
{
return (existing.State, existing.Version);
}
return (null, -1);
}
public async Task<Guid> FindSchemaIdAsync(Guid appId, string name)
{
var schemaEntity =
await Collection.Find(x => x.State.Name == name).Only(x => x.Id)
.FirstOrDefaultAsync();
return schemaEntity != null ? Guid.Parse(schemaEntity.Id) : Guid.Empty;
}
public async Task<IReadOnlyList<Guid>> QuerySchemaIdsAsync(Guid appId)
{
var schemaEntities =
await Collection.Find(x => x.State.AppId == appId).Only(x => x.Id)
.ToListAsync();
return schemaEntities.Select(x => Guid.Parse(x.Id)).ToList();
}
public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion)
{
try
{
await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion,
Update
.Set(x => x.State, value)
.Set(x => x.Version, newVersion),
Upsert);
}
catch (MongoWriteException ex)
{
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingVersion =
await Collection.Find(x => x.Id == key).Only(x => x.Version)
.FirstOrDefaultAsync();
if (existingVersion != null)
{
throw new InconsistentStateException(existingVersion.Version, oldVersion, ex);
}
}
else
{
throw;
}
}
}
}
}

141
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -7,11 +7,16 @@
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
@ -19,48 +24,148 @@ namespace Squidex.Domain.Apps.Entities
{
public sealed class AppProvider : IAppProvider
{
private readonly IStateFactory factory;
private readonly ConcurrentDictionary<string, Guid> appIds = new ConcurrentDictionary<string, Guid>();
private readonly ConcurrentDictionary<Tuple<Guid, string>, Guid> schemaIds = new ConcurrentDictionary<Tuple<Guid, string>, Guid>();
private readonly IAppRepository appRepository;
private readonly IRuleRepository ruleRepository;
private readonly ISchemaRepository schemaRepository;
private readonly IStateFactory stateFactory;
public AppProvider(IStateFactory factory)
public AppProvider(
IAppRepository appRepository,
ISchemaRepository schemaRepository,
IStateFactory stateFactory,
IRuleRepository ruleRepository)
{
Guard.NotNull(factory, nameof(factory));
Guard.NotNull(appRepository, nameof(appRepository));
Guard.NotNull(schemaRepository, nameof(schemaRepository));
Guard.NotNull(stateFactory, nameof(stateFactory));
Guard.NotNull(ruleRepository, nameof(ruleRepository));
this.factory = factory;
this.appRepository = appRepository;
this.schemaRepository = schemaRepository;
this.stateFactory = stateFactory;
this.ruleRepository = ruleRepository;
}
public Task<IAppEntity> GetAppAsync(string appName)
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
{
return null;
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId.ToString());
if (app.Version < 0)
{
throw new DomainObjectNotFoundException(appId.ToString(), typeof(SchemaDomainObject));
}
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id.ToString());
if (schema.Version < 0 || schema.State.IsDeleted)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject));
}
return (app.State, schema.State);
}
public async Task<IAppEntity> GetAppAsync(string appName)
{
var appId = await GetAppIdAsync(appName);
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appName);
if (app.Version < 0)
{
throw new DomainObjectNotFoundException(appName, typeof(SchemaDomainObject));
}
return app.State;
}
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name, bool provideDeleted = false)
{
var schemaId = await GetSchemaIdAsync(appId, name);
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaId.ToString());
if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted))
{
throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(SchemaDomainObject));
}
return schema.State;
}
public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id)
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false)
{
return null;
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id.ToString());
if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted))
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject));
}
return schema.State;
}
public Task<List<IRuleEntity>> GetRulesAsync(string appName)
public async Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId)
{
return null;
var ids = await schemaRepository.QuerySchemaIdsAsync(appId);
var schemas =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<SchemaDomainObject>(id.ToString())));
return schemas.Select(a => (ISchemaEntity)a.State).ToList();
}
public Task<ISchemaEntity> GetSchemaAsync(string appName, Guid id, bool provideDeleted = false)
public async Task<List<IRuleEntity>> GetRulesAsync(Guid appId)
{
return null;
var ids = await ruleRepository.QueryRuleIdsAsync(appId);
var rules =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<RuleDomainObject>(id.ToString())));
return rules.Select(a => (IRuleEntity)a.State).ToList();
}
public Task<ISchemaEntity> GetSchemaAsync(string appName, string name, bool provideDeleted = false)
public async Task<List<IAppEntity>> GetUserApps(string userId)
{
return null;
var ids = await appRepository.QueryUserAppIdsAsync(userId);
var apps =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<AppDomainObject>(id.ToString())));
return apps.Select(a => (IAppEntity)a.State).ToList();
}
public Task<List<ISchemaEntity>> GetSchemasAsync(string appName)
private async Task<Guid> GetAppIdAsync(string name)
{
return null;
var key = name;
if (!appIds.TryGetValue(key, out var id))
{
id = await appRepository.FindAppIdByNameAsync(name);
appIds[key] = id;
}
return id;
}
public Task<List<IAppEntity>> GetUserApps(string userId)
private async Task<Guid> GetSchemaIdAsync(Guid appId, string name)
{
return null;
var key = Tuple.Create(appId, name);
if (!schemaIds.TryGetValue(key, out var id))
{
id = await schemaRepository.FindSchemaIdAsync(appId, name);
schemaIds[key] = id;
}
return id;
}
}
}

5
src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Repositories
{
public interface IAppRepository
{
Task<IReadOnlyList<string>> QueryUserAppNamesAsync(string userId);
Task<Guid> FindAppIdByNameAsync(string name);
Task<IReadOnlyList<Guid>> QueryUserAppIdsAsync(string userId);
}
}

17
src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ChangeContentStatus.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// =========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class ChangeContentStatus : ContentCommand
{
public Status Status { get; set; }
}
}

26
src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs

@ -0,0 +1,26 @@
// ==========================================================================
// ContentCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public abstract class ContentCommand : SchemaCommand, IAggregateCommand
{
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return ContentId; }
}
}
}

17
src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ContentDataCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public abstract class ContentDataCommand : ContentCommand
{
public NamedContentData Data { get; set; }
}
}

15
src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs

@ -0,0 +1,15 @@
// ==========================================================================
// CreateContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class CreateContent : ContentDataCommand
{
public bool Publish { get; set; }
}
}

8
src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs → src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs

@ -1,16 +1,14 @@
// ==========================================================================
// IAssetEventConsumer.cs
// DeleteContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Assets
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public interface IAssetEventConsumer : IEventConsumer
public sealed class DeleteContent : ContentCommand
{
}
}

14
src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs

@ -0,0 +1,14 @@
// ==========================================================================
// PatchContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class PatchContent : ContentDataCommand
{
}
}

14
src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs

@ -0,0 +1,14 @@
// ==========================================================================
// UpdateContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class UpdateContent : ContentDataCommand
{
}
}

158
src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs

@ -0,0 +1,158 @@
// ==========================================================================
// ContentCommandMiddleware.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Dispatching;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentCommandMiddleware : ICommandMiddleware
{
private readonly IAggregateHandler handler;
private readonly IAppProvider appProvider;
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
private readonly IScriptEngine scriptEngine;
public ContentCommandMiddleware(
IAggregateHandler handler,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentRepository contentRepository)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.handler = handler;
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
}
protected async Task On(CreateContent command, CommandContext context)
{
await handler.CreateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanCreate(command);
var operationContext = await CreateContext(command, content, () => "Failed to create content.");
if (command.Publish)
{
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published");
}
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create");
await operationContext.EnrichAsync();
await operationContext.ValidateAsync(false);
content.Create(command);
context.Complete(EntityCreatedResult.Create(command.Data, content.Version));
});
}
protected async Task On(UpdateContent command, CommandContext context)
{
await handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanUpdate(command);
var operationContext = await CreateContext(command, content, () => "Failed to update content.");
await operationContext.ValidateAsync(true);
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update");
content.Update(command);
context.Complete(new ContentDataChangedResult(content.State.Data, content.Version));
});
}
protected async Task On(PatchContent command, CommandContext context)
{
await handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanPatch(command);
var operationContext = await CreateContext(command, content, () => "Failed to patch content.");
await operationContext.ValidateAsync(true);
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch");
content.Patch(command);
context.Complete(new ContentDataChangedResult(content.State.Data, content.Version));
});
}
protected Task On(ChangeContentStatus command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanChangeContentStatus(content.State.Status, command);
var operationContext = await CreateContext(command, content, () => "Failed to patch content.");
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status);
content.ChangeStatus(command);
});
}
protected Task On(DeleteContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanDelete(command);
var operationContext = await CreateContext(command, content, () => "Failed to delete content.");
await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete");
content.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
}
private async Task<ContentOperationContext> CreateContext(ContentCommand command, ContentDomainObject content, Func<string> message)
{
var operationContext =
await ContentOperationContext.CreateAsync(
contentRepository,
content,
command,
appProvider,
assetRepository,
scriptEngine,
message);
return operationContext;
}
}
}

24
src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs

@ -0,0 +1,24 @@
// ==========================================================================
// ContentChangedResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentDataChangedResult : EntitySavedResult
{
public NamedContentData Data { get; }
public ContentDataChangedResult(NamedContentData data, long version)
: base(version)
{
Data = data;
}
}
}

101
src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -0,0 +1,101 @@
// ==========================================================================
// ContentDomainObject.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentDomainObject : DomainObjectBase<ContentDomainObject, ContentState>
{
public ContentDomainObject Create(CreateContent command)
{
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new ContentCreated()));
if (command.Publish)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
}
return this;
}
public ContentDomainObject Delete(DeleteContent command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentDeleted()));
return this;
}
public ContentDomainObject ChangeStatus(ChangeContentStatus command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged()));
return this;
}
public ContentDomainObject Update(UpdateContent command)
{
VerifyCreatedAndNotDeleted();
if (!command.Data.Equals(State.Data))
{
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated()));
}
return this;
}
public ContentDomainObject Patch(PatchContent command)
{
VerifyCreatedAndNotDeleted();
var newData = State.Data.MergeInto(command.Data);
if (!newData.Equals(State.Data))
{
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = newData }));
}
return this;
}
private void VerifyNotCreated()
{
if (State.Data != null)
{
throw new DomainException("Content has already been created.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (State.IsDeleted || State.Data == null)
{
throw new DomainException("Content has already been deleted or not created yet.");
}
}
protected override void OnRaised(Envelope<IEvent> @event)
{
UpdateState(State.Apply(@event));
}
}
}

139
src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -0,0 +1,139 @@
// ==========================================================================
// ContentOperationContext.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.EnrichContent;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentOperationContext
{
private ContentDomainObject content;
private ContentCommand command;
private IContentRepository contentRepository;
private IAssetRepository assetRepository;
private IScriptEngine scriptEngine;
private ISchemaEntity schemaEntity;
private IAppEntity appEntity;
private Func<string> message;
public static async Task<ContentOperationContext> CreateAsync(
IContentRepository contentRepository,
ContentDomainObject content,
ContentCommand command,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
Func<string> message)
{
var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id);
var context = new ContentOperationContext
{
appEntity = appEntity,
assetRepository = assetRepository,
contentRepository = contentRepository,
content = content,
command = command,
message = message,
schemaEntity = schemaEntity,
scriptEngine = scriptEngine
};
return context;
}
public Task EnrichAsync()
{
if (command is ContentDataCommand dataCommand)
{
dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver());
}
return TaskHelper.Done;
}
public async Task ValidateAsync(bool partial)
{
if (command is ContentDataCommand dataCommand)
{
var errors = new List<ValidationError>();
var appId = command.AppId.Id;
var ctx =
new ValidationContext(
(contentIds, schemaId) =>
{
return QueryContentsAsync(appId, schemaId, contentIds);
},
assetIds =>
{
return QueryAssetsAsync(appId, assetIds);
});
if (partial)
{
await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors);
}
else
{
await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors);
}
if (errors.Count > 0)
{
throw new ValidationException(message(), errors.ToArray());
}
}
}
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(Guid appId, IEnumerable<Guid> assetIds)
{
return await assetRepository.QueryAsync(appId, null, new HashSet<Guid>(assetIds), null, int.MaxValue, 0);
}
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable<Guid> contentIds)
{
return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList());
}
public Task ExecuteScriptAndTransformAsync(Func<ISchemaEntity, string> script, object operation)
{
if (command is ContentDataCommand dataCommand)
{
var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data };
dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity));
}
return TaskHelper.Done;
}
public Task ExecuteScriptAsync(Func<ISchemaEntity, string> script, object operation)
{
var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString() };
scriptEngine.Execute(ctx, script(schemaEntity));
return TaskHelper.Done;
}
}
}

11
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.modelBuilder = modelBuilder;
}
public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id)
public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(user, nameof(user));
@ -60,7 +60,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
var schema = await FindSchemaAsync(app, schemaIdOrName);
var content = await contentRepository.FindContentAsync(app, schema, id);
var content =
version > 0 ?
await contentRepository.FindContentAsync(app, schema, id, version) :
await contentRepository.FindContentAsync(app, schema, id);
if (content == null || (content.Status != Status.Published && !isFrontendClient))
{
@ -155,12 +158,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (Guid.TryParse(schemaIdOrName, out var id))
{
schema = await appProvider.GetSchemaAsync(app.Name, id);
schema = await appProvider.GetSchemaAsync(app.Id, id);
}
if (schema == null)
{
schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName);
schema = await appProvider.GetSchemaAsync(app.Id, schemaIdOrName);
}
if (schema == null)

2
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
if (modelContext == null)
{
var allSchemas = await appProvider.GetSchemasAsync(app.Name);
var allSchemas = await appProvider.GetSchemasAsync(app.Id);
modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator);

74
src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -0,0 +1,74 @@
// ==========================================================================
// GuardContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
public static class GuardContent
{
public static void CanCreate(CreateContent command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot created content.", error =>
{
if (command.Data == null)
{
error(new ValidationError("Data cannot be null.", nameof(command.Data)));
}
});
}
public static void CanUpdate(UpdateContent command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot update content.", error =>
{
if (command.Data == null)
{
error(new ValidationError("Data cannot be null.", nameof(command.Data)));
}
});
}
public static void CanPatch(PatchContent command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot patch content.", error =>
{
if (command.Data == null)
{
error(new ValidationError("Data cannot be null.", nameof(command.Data)));
}
});
}
public static void CanChangeContentStatus(Status status, ChangeContentStatus command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot change status.", error =>
{
if (!StatusFlow.Exists(command.Status) || !StatusFlow.CanChange(status, command.Status))
{
error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status)));
}
});
}
public static void CanDelete(DeleteContent command)
{
Guard.NotNull(command, nameof(command));
}
}
}

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

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query);
Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id);
Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1);
Task<ISchemaEntity> FindSchemaAsync(IAppEntity app, string schemaIdOrName);
}

2
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -29,5 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version);
}
}

49
src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -6,17 +6,60 @@
// All rights reserved.
// ==========================================================================
using System;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Contents.State
{
public sealed class ContentState : DomainObjectState<ContentState>
public class ContentState : DomainObjectState<ContentState>, IContentEntity
{
[JsonProperty]
public IdContentData Data { get; set; }
public NamedContentData Data { get; set; }
[JsonProperty]
public string Status { get; set; }
public Guid AppId { get; set; }
[JsonProperty]
public Guid SchemaId { get; set; }
[JsonProperty]
public Status Status { get; set; }
[JsonProperty]
public bool IsDeleted { get; set; }
protected void On(ContentCreated @event)
{
SchemaId = @event.SchemaId.Id;
Data = @event.Data;
}
protected void On(ContentUpdated @event)
{
Data = @event.Data;
}
protected void On(ContentStatusChanged @event)
{
Status = @event.Status;
}
protected void On(ContentDeleted @event)
{
IsDeleted = true;
}
public ContentState Apply(Envelope<IEvent> @event)
{
var payload = (SquidexEvent)@event.Payload;
return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload));
}
}
}

4
src/Squidex.Domain.Apps.Entities/DomainObjectState.cs

@ -14,6 +14,10 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities
{
public abstract class DomainObjectState<T> : Cloneable<T>,
IEntity,
IEntityWithCreatedBy,
IEntityWithLastModifiedBy,
IEntityWithVersion,
IUpdateableEntity,
IUpdateableEntityWithCreatedBy,
IUpdateableEntityWithLastModifiedBy,

10
src/Squidex.Domain.Apps.Entities/IAppProvider.cs

@ -17,17 +17,17 @@ namespace Squidex.Domain.Apps.Entities
{
public interface IAppProvider
{
Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id);
Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id);
Task<IAppEntity> GetAppAsync(string appName);
Task<ISchemaEntity> GetSchemaAsync(string appName, Guid id, bool provideDeleted = false);
Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false);
Task<ISchemaEntity> GetSchemaAsync(string appName, string name, bool provideDeleted = false);
Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name, bool provideDeleted = false);
Task<List<ISchemaEntity>> GetSchemasAsync(string appName);
Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId);
Task<List<IRuleEntity>> GetRulesAsync(string appName);
Task<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task<List<IAppEntity>> GetUserApps(string userId);
}

4
src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
}
else
{
var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider);
var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider);
errors.Foreach(error);
}
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
if (command.Trigger != null)
{
var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider);
var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider);
errors.Foreach(error);
}

4
src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs

@ -26,12 +26,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
SchemaProvider = schemaProvider;
}
public static Task<IEnumerable<ValidationError>> ValidateAsync(string appName, RuleTrigger action, IAppProvider appProvider)
public static Task<IEnumerable<ValidationError>> ValidateAsync(Guid appId, RuleTrigger action, IAppProvider appProvider)
{
Guard.NotNull(action, nameof(action));
Guard.NotNull(appProvider, nameof(appProvider));
var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appName, x));
var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x));
return action.Accept(visitor);
}

2
src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs

@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories
{
public interface IRuleRepository
{
Task<IReadOnlyList<string>> QueryRuleIdsAsync(Guid appId);
Task<IReadOnlyList<Guid>> QueryRuleIdsAsync(Guid appId);
}
}

2
src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
{
if (@event.Payload is AppEvent appEvent)
{
var rules = await appProvider.GetRulesAsync(appEvent.AppId.Name);
var rules = await appProvider.GetRulesAsync(appEvent.AppId.Id);
foreach (var ruleEntity in rules)
{

2
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
error(new ValidationError("Name must be a valid slug.", nameof(command.Name)));
}
if (await appProvider.GetSchemaAsync(command.AppId.Name, command.Name) != null)
if (await appProvider.GetSchemaAsync(command.AppId.Id, command.Name) != null)
{
error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name)));
}

4
src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs

@ -14,8 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Repositories
{
public interface ISchemaRepository
{
Task<string> FindSchemaNameAsync(Guid schemaId);
Task<Guid> FindSchemaIdAsync(Guid appId, string name);
Task<IReadOnlyList<string>> QuerySchemaNamesAsync(Guid appId);
Task<IReadOnlyList<Guid>> QuerySchemaIdsAsync(Guid appId);
}
}

23
src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs

@ -6,6 +6,8 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
using MongoDB.Driver;
@ -31,5 +33,26 @@ namespace Squidex.Infrastructure.MongoDb
return true;
}
public static IFindFluent<TDocument, TDocument> Only<TDocument>(this IFindFluent<TDocument, TDocument> find,
Expression<Func<TDocument, object>> include)
{
return find.Project<TDocument>(Builders<TDocument>.Projection.Include(include));
}
public static IFindFluent<TDocument, TDocument> Only<TDocument>(this IFindFluent<TDocument, TDocument> find,
Expression<Func<TDocument, object>> include1,
Expression<Func<TDocument, object>> include2)
{
return find.Project<TDocument>(Builders<TDocument>.Projection.Include(include1).Include(include2));
}
public static IFindFluent<TDocument, TDocument> Only<TDocument>(this IFindFluent<TDocument, TDocument> find,
Expression<Func<TDocument, object>> include1,
Expression<Func<TDocument, object>> include2,
Expression<Func<TDocument, object>> include3)
{
return find.Project<TDocument>(Builders<TDocument>.Projection.Include(include1).Include(include2).Include(include3));
}
}
}

4
src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -59,8 +59,8 @@ namespace Squidex.Infrastructure.States
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingVersion =
await Collection.Find(x => x.Id == key)
.Project<MongoState<T>>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync();
await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version)
.FirstOrDefaultAsync();
if (existingVersion != null)
{

2
src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs

@ -17,8 +17,6 @@ namespace Squidex.Infrastructure.UsageTracking
{
public sealed class MongoUsageStore : MongoRepositoryBase<MongoUsage>, IUsageStore
{
private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true };
public MongoUsageStore(IMongoDatabase database)
: base(database)
{

9
src/Squidex.Infrastructure/Commands/DomainObjectBase.cs

@ -18,18 +18,17 @@ namespace Squidex.Infrastructure.Commands
public abstract class DomainObjectBase<TBase, TState> : IDomainObject where TState : new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private int version = -1;
private TState state = new TState();
private IPersistence<TState> persistence;
public TState State
public long Version
{
get { return state; }
get { return persistence.Version; }
}
public int Version
public TState State
{
get { return version; }
get { return state; }
}
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents()

2
src/Squidex.Infrastructure/Commands/IDomainObject.cs

@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Commands
{
public interface IDomainObject : IStatefulObject
{
int Version { get; }
long Version { get; }
Task WriteAsync(ISemanticLog log);
}

2
src/Squidex.Infrastructure/States/IPersistence.cs

@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.States
{
public interface IPersistence<TState>
{
long Version { get; }
Task WriteEventsAsync(params Envelope<IEvent>[] @events);
Task WriteSnapshotAsync(TState state);

13
src/Squidex.Infrastructure/States/Persistence.cs

@ -26,6 +26,11 @@ namespace Squidex.Infrastructure.States
private long positionSnapshot = -1;
private long positionEvent = -1;
public long Version
{
get { return Math.Max(positionEvent, positionSnapshot); }
}
public Persistence(string ownerKey,
Action invalidate,
IEventStore eventStore,
@ -85,17 +90,17 @@ namespace Squidex.Infrastructure.States
}
}
var maxVersion = Math.Max(positionEvent, positionSnapshot);
var newVersion = Version;
if (expectedVersion.HasValue && expectedVersion.Value != maxVersion)
if (expectedVersion.HasValue && expectedVersion.Value != newVersion)
{
if (maxVersion == -1)
if (newVersion == -1)
{
throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner));
}
else
{
throw new DomainObjectVersionException(ownerKey, typeof(TOwner), maxVersion, expectedVersion.Value);
throw new DomainObjectVersionException(ownerKey, typeof(TOwner), newVersion, expectedVersion.Value);
}
}
}

7
src/Squidex/Areas/Api/Controllers/ApiController.cs

@ -9,7 +9,7 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
@ -41,6 +41,11 @@ namespace Squidex.Areas.Api.Controllers
get { return App.Name; }
}
protected Guid AppId
{
get { return App.Id; }
}
protected ApiController(ICommandBus commandBus)
{
Guard.NotNull(commandBus, nameof(commandBus));

2
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Write.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;

4
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -12,8 +12,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Write.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;

2
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -15,7 +15,7 @@ using Microsoft.Extensions.Primitives;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Write.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;

6
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -13,9 +13,9 @@ using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Write.Apps.Commands;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;

2
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -11,7 +11,7 @@ using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;

8
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -16,10 +16,10 @@ using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Assets.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;

2
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs

@ -8,7 +8,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Write.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models

4
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs

@ -7,8 +7,8 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{

4
src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs

@ -10,7 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(0)]
public async Task<IActionResult> GetSwagger(string app)
{
var schemas = await appProvider.GetSchemasAsync(AppName);
var schemas = await appProvider.GetSchemasAsync(AppId);
var swaggerDocument = await schemasSwaggerGenerator.Generate(App, schemas);

21
src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs

@ -16,10 +16,9 @@ using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -33,17 +32,14 @@ namespace Squidex.Areas.Api.Controllers.Contents
public sealed class ContentsController : ApiController
{
private readonly IContentQueryService contentQuery;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IGraphQLService graphQl;
public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery,
IContentVersionLoader contentVersionLoader,
IGraphQLService graphQl)
: base(commandBus)
{
this.contentQuery = contentQuery;
this.contentVersionLoader = contentVersionLoader;
this.graphQl = graphQl;
}
@ -142,9 +138,16 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string name, Guid id, int version)
{
var contentData = await contentVersionLoader.LoadAsync(App.Id, id, version);
var content = await contentQuery.FindContentAsync(App, name, User, id, version);
var response = contentData;
var response = SimpleMapper.Map(content.Content, new ContentDto());
if (content.Content.Data != null)
{
var isFrontendClient = User.IsFrontendClient();
response.Data = content.Content.Data.ToApiModel(content.Schema.SchemaDef, App.LanguagesConfig, !isFrontendClient);
}
Response.Headers["ETag"] = new StringValues(version.ToString());

4
src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs

@ -16,8 +16,8 @@ using NSwag;
using NSwag.AspNetCore;
using NSwag.SwaggerGeneration;
using Squidex.Config;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;

2
src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs

@ -10,7 +10,7 @@ using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Write.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;

2
src/Squidex/Areas/Api/Controllers/History/HistoryController.cs

@ -11,7 +11,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.History.Models;
using Squidex.Domain.Apps.Read.History.Repositories;
using Squidex.Domain.Apps.Entities.History.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;

6
src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs

@ -12,8 +12,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Plans.Models;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Write.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Plans
{
CurrentPlanId = planId,
Plans = appPlansProvider.GetAvailablePlans().Select(x => SimpleMapper.Map(x, new PlanDto())).ToList(),
PlanOwner = App.PlanOwner,
PlanOwner = App.Plan?.Owner.Identifier,
HasPortal = appPlansBillingManager.HasPortal
};

4
src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs

@ -7,8 +7,8 @@
// ==========================================================================
using System;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters

2
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs

@ -10,7 +10,7 @@ using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Entities.Rules;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{

8
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -14,9 +14,9 @@ using NodaTime;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Areas.Api.Controllers.Rules.Models.Converters;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -59,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[ApiCosts(1)]
public async Task<IActionResult> GetRules(string app)
{
var rules = await appProvider.GetRulesAsync(AppName);
var rules = await appProvider.GetRulesAsync(AppId);
var response = rules.Select(r => r.ToModel());

4
src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs

@ -8,8 +8,8 @@
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Write.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters

2
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -10,7 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Write.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;

12
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -15,9 +15,9 @@ using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Areas.Api.Controllers.Schemas.Models.Converters;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Write.Schemas.Commands;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -56,7 +56,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiCosts(0)]
public async Task<IActionResult> GetSchemas(string app)
{
var schemas = await appProvider.GetSchemasAsync(AppName);
var schemas = await appProvider.GetSchemasAsync(AppId);
var response = schemas.Select(s => s.ToModel()).ToList();
@ -83,11 +83,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
if (Guid.TryParse(name, out var id))
{
entity = await appProvider.GetSchemaAsync(AppName, id);
entity = await appProvider.GetSchemaAsync(AppId, id);
}
else
{
entity = await appProvider.GetSchemaAsync(AppName, name);
entity = await appProvider.GetSchemaAsync(AppId, name);
}
if (entity == null || entity.IsDeleted)

4
src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -12,8 +12,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Statistics.Models;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline;

2
src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs

@ -15,7 +15,7 @@ using IdentityServer4.Stores;
using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
namespace Squidex.Areas.IdentityServer.Config

2
src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs

@ -8,7 +8,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Security;
namespace Squidex.Areas.Portal.Middlewares

36
src/Squidex/Config/Domain/ReadServices.cs

@ -13,23 +13,20 @@ using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Actions;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Apps.Services.Implementations;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.Edm;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.History;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.State;
using Squidex.Domain.Apps.Read.State.Grains;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.States;
@ -103,15 +100,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<DefaultEventNotifier>()
.As<IEventNotifier>();
services.AddSingletonAs<AppStateEventConsumer>()
.As<IEventConsumer>();
services.AddSingletonAs<RuleEnqueuer>()
.As<IEventConsumer>();
services.AddSingletonAs<IEventConsumer>(c =>
new CompoundEventConsumer(c.GetServices<IAssetEventConsumer>().ToArray()));
services.AddSingletonAs(c =>
{
var allEventConsumers = c.GetServices<IEventConsumer>();
@ -120,11 +111,6 @@ namespace Squidex.Config.Domain
});
services.AddSingletonAs<EdmModelBuilder>();
services.AddTransient(typeof(DomainObjectWrapper<>));
services.AddTransient<AppStateGrain>();
services.AddTransient<AppUserGrain>();
services.AddSingleton<RuleService>();
}
}

66
src/Squidex/Config/Domain/StoreServices.cs

@ -13,22 +13,30 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.History;
using Squidex.Domain.Apps.Read.History.Repositories;
using Squidex.Domain.Apps.Read.MongoDb.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Contents;
using Squidex.Domain.Apps.Read.MongoDb.History;
using Squidex.Domain.Apps.Read.MongoDb.Rules;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Apps.State;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.History.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Apps;
using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.History;
using Squidex.Domain.Apps.Entities.MongoDb.Rules;
using Squidex.Domain.Apps.Entities.MongoDb.Schemas;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Rules.State;
using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Domain.Users;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Shared.Users;
@ -55,8 +63,8 @@ namespace Squidex.Config.Domain
.As<IXmlRepository>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore>()
services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<EventConsumerState>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoUserStore(mongoDatabase))
@ -78,12 +86,33 @@ namespace Squidex.Config.Domain
.As<IUsageStore>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase))
.As<IRuleEventRepository>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase))
.As<IAppRepository>()
.As<ISnapshotStore<AppState>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase))
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService<IAppProvider>()))
.As<IContentRepository>()
.As<ISnapshotStore<ContentState>>()
.As<IEventConsumer>();
services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase))
.As<IRuleEventRepository>()
services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase))
.As<IRuleRepository>()
.As<ISnapshotStore<RuleState>>()
.As<IEventConsumer>();
services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase))
.As<ISchemaRepository>()
.As<ISnapshotStore<AppState>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices<IHistoryEventsCreator>()))
@ -91,14 +120,9 @@ namespace Squidex.Config.Domain
.As<IEventConsumer>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase))
.As<IAssetRepository>()
.As<IAssetEventConsumer>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase))
.As<IAssetStatsRepository>()
.As<IAssetEventConsumer>()
.As<IEventConsumer>()
.As<IExternalSystem>();
}
});

31
src/Squidex/Config/Domain/WriteServices.cs

@ -7,13 +7,13 @@
// ==========================================================================
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Write.Apps;
using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Rules;
using Squidex.Domain.Apps.Write.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.State.SchemaDefs;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline.CommandMiddlewares;
@ -30,9 +30,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<JintScriptEngine>()
.As<IScriptEngine>();
services.AddSingletonAs<ContentVersionLoader>()
.As<IContentVersionLoader>();
services.AddSingletonAs<ETagCommandMiddleware>()
.As<ICommandMiddleware>();
@ -63,17 +60,11 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, -1)));
services.AddSingletonAs<DomainObjectFactoryFunction<RuleDomainObject>>(c => (id => new RuleDomainObject(id, -1)));
services.AddSingletonAs<DomainObjectFactoryFunction<AssetDomainObject>>(c => (id => new AssetDomainObject(id, -1)));
services.AddSingletonAs<DomainObjectFactoryFunction<ContentDomainObject>>(c => (id => new ContentDomainObject(id, -1)));
services.AddSingletonAs<DomainObjectFactoryFunction<SchemaDomainObject>>(c =>
{
var fieldRegistry = c.GetRequiredService<FieldRegistry>();
return id => new SchemaDomainObject(id, -1, fieldRegistry);
});
services.AddTransient<AppDomainObject>();
services.AddTransient<AssetDomainObject>();
services.AddTransient<ContentDomainObject>();
services.AddTransient<RuleDomainObject>();
services.AddTransient<SchemaDomainObject>();
}
}
}

2
src/Squidex/Config/MyUsageOptions.cs

@ -6,7 +6,7 @@
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Read.Apps.Services.Implementations;
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations;
namespace Squidex.Config
{

2
src/Squidex/Pipeline/ApiCostsFilter.cs

@ -11,7 +11,7 @@ using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.UsageTracking;
namespace Squidex.Pipeline

4
src/Squidex/Pipeline/AppApiFilter.cs

@ -9,8 +9,8 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
namespace Squidex.Pipeline
{

2
src/Squidex/Pipeline/AppPermissionAttribute.cs

@ -12,7 +12,7 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;

2
src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs

@ -10,7 +10,7 @@ using System;
using System.Security;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Write;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;

2
src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs

@ -9,7 +9,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Write;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;

10
src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs

@ -9,10 +9,8 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Squidex.Domain.Apps.Read;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Write;
using Squidex.Domain.Apps.Write.Schemas;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -44,11 +42,11 @@ namespace Squidex.Pipeline.CommandMiddlewares
if (Guid.TryParse(schemaName, out var id))
{
schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Name, id);
schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, id);
}
else
{
schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Name, schemaName);
schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, schemaName);
}
if (schema == null)

10
src/Squidex/Pipeline/GraphQLUrlGenerator.cs

@ -8,11 +8,11 @@
using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Assets;
namespace Squidex.Pipeline

2
src/Squidex/Pipeline/IAppFeature.cs

@ -6,7 +6,7 @@
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Entities.Apps;
namespace Squidex.Pipeline
{

7
src/Squidex/Squidex.csproj

@ -31,9 +31,11 @@
<ItemGroup>
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Entities.MongoDb\Squidex.Domain.Apps.Entities.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" />
<ProjectReference Include="..\Squidex.Domain.Users.MongoDb\Squidex.Domain.Users.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Domain.Users\Squidex.Domain.Users.csproj" />
<ProjectReference Include="..\Squidex.Domain.Users.MongoDb\Squidex.Domain.Users.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.Azure\Squidex.Infrastructure.Azure.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.GetEventStore\Squidex.Infrastructure.GetEventStore.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj" />
@ -41,9 +43,6 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.Redis\Squidex.Infrastructure.Redis.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Read\Squidex.Domain.Apps.Read.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Read.MongoDb\Squidex.Domain.Apps.Read.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Write\Squidex.Domain.Apps.Write.csproj" />
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>

244
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs

@ -0,0 +1,244 @@
// ==========================================================================
// ContentCommandMiddlewareTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
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.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentCommandMiddlewareTests : HandlerTestBase<ContentDomainObject>
{
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly ClaimsPrincipal user = new ClaimsPrincipal();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE);
private readonly Guid contentId = Guid.NewGuid();
private readonly ContentDomainObject content = new ContentDomainObject();
private readonly ContentCommandMiddleware sut;
private readonly NamedContentData invalidData =
new NamedContentData()
.AddField("my-field1", new ContentFieldData()
.AddValue(null))
.AddField("my-field2", new ContentFieldData()
.AddValue(1));
private readonly NamedContentData data =
new NamedContentData()
.AddField("my-field1", new ContentFieldData()
.AddValue(1))
.AddField("my-field2", new ContentFieldData()
.AddValue(1));
private readonly NamedContentData patch =
new NamedContentData()
.AddField("my-field1", new ContentFieldData()
.AddValue(1));
public ContentCommandMiddlewareTests()
{
var schemaDef =
new Schema("my-schema")
.AddField(new NumberField(1, "my-field1", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = true }))
.AddField(new NumberField(2, "my-field2", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = false }));
sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy<IAssetRepository>(), scriptEngine, A.Dummy<IContentRepository>());
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig);
A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
A.CallTo(() => schema.ScriptCreate).Returns("<create-script>");
A.CallTo(() => schema.ScriptChange).Returns("<change-script>");
A.CallTo(() => schema.ScriptUpdate).Returns("<update-script>");
A.CallTo(() => schema.ScriptDelete).Returns("<delete-script>");
A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema));
}
[Fact]
public async Task Create_should_throw_exception_if_data_is_not_valid()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(invalidData);
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user });
await TestCreate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Create_should_create_content()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user });
await TestCreate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(data, context.Result<EntityCreatedResult<NamedContentData>>().IdOrValue);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>")).MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustNotHaveHappened();
}
[Fact]
public async Task Create_should_also_invoke_publish_script_when_publishing()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user, Publish = true });
await TestCreate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(data, context.Result<EntityCreatedResult<NamedContentData>>().IdOrValue);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>")).MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustHaveHappened();
}
[Fact]
public async Task Update_should_throw_exception_if_data_is_not_valid()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(invalidData);
CreateContent();
var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user });
await TestUpdate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Update_should_update_domain_object()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
CreateContent();
var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(data, context.Result<ContentDataChangedResult>().Data);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>")).MustHaveHappened();
}
[Fact]
public async Task Patch_should_throw_exception_if_data_is_not_valid()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(invalidData);
CreateContent();
var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user });
await TestUpdate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Patch_should_update_domain_object()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)).Returns(patch);
CreateContent();
var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.NotNull(context.Result<ContentDataChangedResult>().Data);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>")).MustHaveHappened();
}
[Fact]
public async Task ChangeStatus_should_publish_domain_object()
{
CreateContent();
var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustHaveHappened();
}
[Fact]
public async Task Delete_should_update_domain_object()
{
CreateContent();
var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(command);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<delete-script>")).MustHaveHappened();
}
private void CreateContent()
{
content.Create(new CreateContent { Data = data });
}
}
}

269
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -0,0 +1,269 @@
// ==========================================================================
// ContentDomainObjectTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentDomainObjectTests : HandlerTestBase<ContentDomainObject>
{
private readonly NamedContentData data =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("iv", 1));
private readonly NamedContentData otherData =
new NamedContentData()
.AddField("field2",
new ContentFieldData()
.AddValue("iv", 2));
private readonly Guid contentId = Guid.NewGuid();
private readonly ContentDomainObject sut = new ContentDomainObject();
[Fact]
public void Create_should_throw_exception_if_created()
{
sut.Create(new CreateContent { Data = data });
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateContentCommand(new CreateContent { Data = data }));
});
}
[Fact]
public void Create_should_create_events()
{
sut.Create(CreateContentCommand(new CreateContent { Data = data }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
}
[Fact]
public void Create_should_also_publish_if_set_to_true()
{
sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data }),
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
}
[Fact]
public void Update_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateContentCommand(new UpdateContent { Data = data }));
});
}
[Fact]
public void Update_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateContentCommand(new UpdateContent()));
});
}
[Fact]
public void Update_should_create_events()
{
CreateContent();
sut.Update(CreateContentCommand(new UpdateContent { Data = otherData }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = otherData })
);
}
[Fact]
public void Update_should_not_create_event_for_same_data()
{
CreateContent();
UpdateContent();
sut.Update(CreateContentCommand(new UpdateContent { Data = data }));
sut.GetUncomittedEvents().Should().BeEmpty();
}
[Fact]
public void Patch_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Patch(CreateContentCommand(new PatchContent { Data = data }));
});
}
[Fact]
public void Patch_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Patch(CreateContentCommand(new PatchContent()));
});
}
[Fact]
public void Patch_should_create_events()
{
CreateContent();
sut.Patch(CreateContentCommand(new PatchContent { Data = otherData }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = otherData })
);
}
[Fact]
public void Patch_should_not_create_event_for_same_data()
{
CreateContent();
UpdateContent();
sut.Patch(CreateContentCommand(new PatchContent { Data = data }));
sut.GetUncomittedEvents().Should().BeEmpty();
}
[Fact]
public void ChangeStatus_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
});
}
[Fact]
public void ChangeStatus_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
});
}
[Fact]
public void ChangeStatus_should_refresh_properties_and_create_events()
{
CreateContent();
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published }));
Assert.Equal(Status.Published, sut.State.Status);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
}
[Fact]
public void Delete_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateContentCommand(new DeleteContent()));
});
}
[Fact]
public void Delete_should_throw_exception_if_already_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateContentCommand(new DeleteContent()));
});
}
[Fact]
public void Delete_should_update_properties_and_create_events()
{
CreateContent();
sut.Delete(CreateContentCommand(new DeleteContent()));
Assert.True(sut.State.IsDeleted);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentDeleted())
);
}
private void CreateContent()
{
sut.Create(CreateContentCommand(new CreateContent { Data = data }));
sut.ClearUncommittedEvents();
}
private void UpdateContent()
{
sut.Update(CreateContentCommand(new UpdateContent { Data = data }));
sut.ClearUncommittedEvents();
}
private void ChangeStatus(Status status)
{
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status }));
sut.ClearUncommittedEvents();
}
private void DeleteContent()
{
sut.Delete(CreateContentCommand(new DeleteContent()));
sut.ClearUncommittedEvents();
}
protected T CreateContentEvent<T>(T @event) where T : ContentEvent
{
@event.ContentId = contentId;
return CreateEvent(@event);
}
protected T CreateContentCommand<T>(T command) where T : ContentCommand
{
command.ContentId = contentId;
return CreateCommand(command);
}
}
}

70
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs

@ -0,0 +1,70 @@
// ==========================================================================
// ContentEventTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Events.Contents.Old;
using Squidex.Infrastructure;
using Xunit;
#pragma warning disable CS0612 // Type or member is obsolete
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentEventTests
{
private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString());
private readonly NamedId<Guid> appId = new NamedId<Guid>(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = new NamedId<Guid>(Guid.NewGuid(), "my-schema");
private readonly Guid contentId = Guid.NewGuid();
[Fact]
public void Should_migrate_content_published_to_content_status_changed()
{
var source = CreateEvent(new ContentPublished());
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published }));
}
[Fact]
public void Should_migrate_content_unpublished_to_content_status_changed()
{
var source = CreateEvent(new ContentUnpublished());
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft }));
}
[Fact]
public void Should_migrate_content_restored_to_content_status_changed()
{
var source = CreateEvent(new ContentRestored());
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft }));
}
[Fact]
public void Should_migrate_content_archived_to_content_status_changed()
{
var source = CreateEvent(new ContentArchived());
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived }));
}
private T CreateEvent<T>(T contentEvent) where T : ContentEvent
{
contentEvent.Actor = actor;
contentEvent.AppId = appId;
contentEvent.SchemaId = schemaId;
contentEvent.ContentId = contentId;
return contentEvent;
}
}
}

14
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_schema_from_id_if_string_is_guid()
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
var result = await sut.FindSchemaAsync(app, schemaId.ToString());
@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_schema_from_name_if_string_not_guid()
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema", false))
.Returns(schema);
var result = await sut.FindSchemaAsync(app, "my-schema");
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_throw_if_schema_not_found()
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema", false))
.Returns((ISchemaEntity)null);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.FindSchemaAsync(app, "my-schema"));
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_content_from_repository_and_transform()
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId))
.Returns(content);
@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_throw_if_content_to_find_does_not_exist()
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId))
.Returns((IContentEntity)null);
@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private void SetupFakeWithIdQuery(Status[] status, HashSet<Guid> ids)
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids))
.Returns(new List<IContentEntity> { content });
@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private void SetupFakeWithOdataQuery(Status[] status)
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored))
.Returns(new List<IContentEntity> { content });

10
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs

@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var allSchemas = new List<ISchemaEntity> { schema };
A.CallTo(() => appProvider.GetSchemasAsync(appName)).Returns(allSchemas);
A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas);
sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator());
}
@ -391,7 +391,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}}
}}";
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId))
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1))
.Returns((schema, content));
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query });
@ -483,7 +483,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var refContents = new List<IContentEntity> { contentRef };
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId))
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1))
.Returns((schema, content));
A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A<HashSet<Guid>>.That.Matches(x => x.Contains(contentRefId))))
@ -543,7 +543,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var refAssets = new List<IAssetEntity> { assetRef };
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId))
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1))
.Returns((schema, content));
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A<HashSet<Guid>>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0))
@ -602,7 +602,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}}
}}";
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId))
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1))
.Returns((schema, content));
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query });

99
tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

@ -0,0 +1,99 @@
// ==========================================================================
// GuardContentTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Guard
{
public class GuardContentTests
{
[Fact]
public void CanCreate_should_throw_exception_if_data_is_null()
{
var command = new CreateContent();
Assert.Throws<ValidationException>(() => GuardContent.CanCreate(command));
}
[Fact]
public void CanCreate_should_not_throw_exception_if_data_is_not_null()
{
var command = new CreateContent { Data = new NamedContentData() };
GuardContent.CanCreate(command);
}
[Fact]
public void CanUpdate_should_throw_exception_if_data_is_null()
{
var command = new UpdateContent();
Assert.Throws<ValidationException>(() => GuardContent.CanUpdate(command));
}
[Fact]
public void CanUpdate_should_not_throw_exception_if_data_is_not_null()
{
var command = new UpdateContent { Data = new NamedContentData() };
GuardContent.CanUpdate(command);
}
[Fact]
public void CanPatch_should_throw_exception_if_data_is_null()
{
var command = new PatchContent();
Assert.Throws<ValidationException>(() => GuardContent.CanPatch(command));
}
[Fact]
public void CanPatch_should_not_throw_exception_if_data_is_not_null()
{
var command = new PatchContent { Data = new NamedContentData() };
GuardContent.CanPatch(command);
}
[Fact]
public void CanChangeContentStatus_should_throw_exception_if_status_not_valid()
{
var command = new ChangeContentStatus { Status = (Status)10 };
Assert.Throws<ValidationException>(() => GuardContent.CanChangeContentStatus(Status.Archived, command));
}
[Fact]
public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid()
{
var command = new ChangeContentStatus { Status = Status.Published };
Assert.Throws<ValidationException>(() => GuardContent.CanChangeContentStatus(Status.Archived, command));
}
[Fact]
public void CanChangeContentStatus_not_should_throw_exception_if_status_flow_valid()
{
var command = new ChangeContentStatus { Status = Status.Published };
GuardContent.CanChangeContentStatus(Status.Draft, command);
}
[Fact]
public void CanPatch_should_not_throw_exception()
{
var command = new DeleteContent();
GuardContent.CanDelete(command);
}
}
}

4
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
/*
using System;
using System.Collections.Immutable;
using FakeItEasy;
@ -390,5 +389,4 @@ namespace Squidex.Domain.Apps.Entities.Contents
return query;
}
}
}
*/
}

2
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
public GuardRuleTests()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId.Name, A<Guid>.Ignored, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
}

14
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs

@ -19,12 +19,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
public class ContentChangedTriggerTests
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly string appName = "my-app";
private readonly Guid appId = Guid.NewGuid();
[Fact]
public async Task Should_add_error_if_schemas_ids_are_not_valid()
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, A<Guid>.Ignored, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, A<Guid>.Ignored, false))
.Returns(Task.FromResult<ISchemaEntity>(null));
var trigger = new ContentChangedTrigger
@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
)
};
var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider);
var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider);
Assert.NotEmpty(errors);
}
@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
{
var trigger = new ContentChangedTrigger();
var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider);
var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider);
Assert.Empty(errors);
}
@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
Schemas = ImmutableList<ContentChangedTriggerSchema>.Empty
};
var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider);
var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider);
Assert.Empty(errors);
}
@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
[Fact]
public async Task Should_not_add_error_if_schemas_ids_are_valid()
{
A.CallTo(() => appProvider.GetSchemaAsync(appName, A<Guid>.Ignored, false))
A.CallTo(() => appProvider.GetSchemaAsync(appId, A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
var trigger = new ContentChangedTrigger
@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
)
};
var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider);
var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider);
Assert.Empty(errors);
}

2
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
public RuleCommandMiddlewareTests()
{
A.CallTo(() => appProvider.GetSchemaAsync(A<string>.Ignored, A<Guid>.Ignored, false))
A.CallTo(() => appProvider.GetSchemaAsync(A<Guid>.Ignored, A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
sut = new RuleCommandMiddleware(Handler, appProvider);

2
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2);
A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3);
A.CallTo(() => appProvider.GetRulesAsync(appId.Name))
A.CallTo(() => appProvider.GetRulesAsync(appId.Id))
.Returns(new List<IRuleEntity> { ruleEntity1, ruleEntity2, ruleEntity3 });
A.CallTo(() => ruleService.CreateJob(rule1, @event))

2
tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -12,7 +12,9 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" />
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Entities.MongoDb\Squidex.Domain.Apps.Entities.MongoDb.csproj" />
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
</ItemGroup>

28
tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.Commands.TestHelpers;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
using Xunit;
@ -19,42 +20,29 @@ namespace Squidex.Infrastructure.Commands
{
public class AggregateHandlerTests
{
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>();
private readonly IStore store = A.Fake<IStore>();
private readonly IStateFactory stateFactory = A.Fake<IStateFactory>();
private readonly IPersistence<object> persistence = A.Fake<IPersistence<object>>();
private readonly Envelope<IEvent> event1 = new Envelope<IEvent>(new MyEvent());
private readonly Envelope<IEvent> event2 = new Envelope<IEvent>(new MyEvent());
private readonly DomainObjectFactoryFunction<MyDomainObject> factory;
private readonly CommandContext context;
private readonly AggregateHandler sut;
private readonly DomainObjectWrapper<MyDomainObject> domainObjectWrapper = new DomainObjectWrapper<MyDomainObject>();
private readonly Guid domainObjectId = Guid.NewGuid();
private readonly MyDomainObject domainObject;
private readonly MyDomainObject domainObject = new MyDomainObject();
private readonly AggregateHandler sut;
public AggregateHandlerTests()
{
factory = new DomainObjectFactoryFunction<MyDomainObject>(id => domainObject);
domainObject =
new MyDomainObject(domainObjectId, 1)
.RaiseNewEvent(event1)
.RaiseNewEvent(event2);
context = new CommandContext(new MyCommand { AggregateId = domainObject.Id });
context = new CommandContext(new MyCommand { AggregateId = domainObjectId });
A.CallTo(() => store.WithEventSourcing<MyDomainObject>(domainObjectId.ToString(), A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence);
A.CallTo(() => serviceProvider.GetService(factory.GetType()))
.Returns(factory);
A.CallTo(() => stateFactory.GetDetachedAsync<DomainObjectWrapper<MyDomainObject>>(domainObject.Id.ToString()))
.Returns(Task.FromResult(domainObjectWrapper));
sut = new AggregateHandler(stateFactory, serviceProvider);
A.CallTo(() => stateFactory.CreateAsync<MyDomainObject>(domainObjectId.ToString()))
.Returns(Task.FromResult(domainObject));
domainObjectWrapper.ActivateAsync(domainObjectId.ToString(), store).Wait();
sut = new AggregateHandler(stateFactory, serviceProvider, log);
}
[Fact]

61
tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs

@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.Commands
var domainObjectId = Guid.NewGuid();
var domainObjectVersion = 123;
var sut = new MyDomainObject(domainObjectId, domainObjectVersion);
var sut = new MyDomainObject();
Assert.Equal(domainObjectId, sut.Id);
Assert.Equal(domainObjectVersion, sut.Version);
@ -34,18 +34,16 @@ namespace Squidex.Infrastructure.Commands
var event1 = new MyEvent();
var event2 = new MyEvent();
var sut = new MyDomainObject(Guid.NewGuid(), 10);
IAggregate aggregate = sut;
var sut = new MyDomainObject();
sut.RaiseNewEvent(event1);
sut.RaiseNewEvent(event2);
Assert.Equal(12, sut.Version);
Assert.Equal(new IEvent[] { event1, event2 }, aggregate.GetUncomittedEvents().Select(x => x.Payload).ToArray());
Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray());
aggregate.ClearUncommittedEvents();
sut.ClearUncommittedEvents();
Assert.Equal(0, sut.GetUncomittedEvents().Count);
}
@ -56,58 +54,13 @@ namespace Squidex.Infrastructure.Commands
var event1 = new MyEvent();
var event2 = new MyEvent();
var sut = new MyDomainObject(Guid.NewGuid(), 10);
IAggregate aggregate = sut;
var sut = new MyDomainObject();
aggregate.ApplyEvent(new Envelope<IEvent>(event1));
aggregate.ApplyEvent(new Envelope<IEvent>(event2));
sut.RaiseEvent(new Envelope<IEvent>(event1));
sut.RaiseEvent(new Envelope<IEvent>(event2));
Assert.Equal(12, sut.Version);
Assert.Equal(0, sut.GetUncomittedEvents().Count);
}
[Fact]
public void Should_make_correct_equal_comparisons()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var user1a = new MyDomainObject(id1, 1);
var user1b = new MyDomainObject(id1, 2);
var user2a = new MyDomainObject(id2, 2);
Assert.True(user1a.Equals(user1b));
Assert.False(user1a.Equals(user2a));
}
[Fact]
public void Should_make_correct_object_equal_comparisons()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var user1a = new MyDomainObject(id1, 1);
object user1b = new MyDomainObject(id1, 2);
object user2a = new MyDomainObject(id2, 2);
Assert.True(user1a.Equals(user1b));
Assert.False(user1a.Equals(user2a));
}
[Fact]
public void Should_provide_correct_hash_codes()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var user1a = new MyDomainObject(id1, 1);
var user1b = new MyDomainObject(id1, 2);
var user2a = new MyDomainObject(id2, 2);
Assert.Equal(user1a.GetHashCode(), user1b.GetHashCode());
Assert.NotEqual(user1a.GetHashCode(), user2a.GetHashCode());
}
}
}

11
tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs

@ -11,13 +11,8 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.Commands.TestHelpers
{
internal sealed class MyDomainObject : DomainObjectBase
internal sealed class MyDomainObject : DomainObjectBase<MyDomainObject, object>
{
public MyDomainObject(Guid id, int version)
: base(id, version)
{
}
public MyDomainObject RaiseNewEvent(IEvent @event)
{
RaiseEvent(@event);
@ -31,9 +26,5 @@ namespace Squidex.Infrastructure.Commands.TestHelpers
return this;
}
protected override void DispatchEvent(Envelope<IEvent> @event)
{
}
}
}

4
tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs

@ -33,8 +33,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
A.CallTo(() => consumer1.Name).Returns(consumerName1);
A.CallTo(() => consumer2.Name).Returns(consumerName2);
A.CallTo(() => factory.GetDetachedAsync<EventConsumerGrain>(consumerName1)).Returns(actor1);
A.CallTo(() => factory.GetDetachedAsync<EventConsumerGrain>(consumerName2)).Returns(actor2);
A.CallTo(() => factory.CreateAsync<EventConsumerGrain>(consumerName1)).Returns(actor1);
A.CallTo(() => factory.CreateAsync<EventConsumerGrain>(consumerName2)).Returns(actor2);
sut = new EventConsumerGrainManager(new IEventConsumer[] { consumer1, consumer2 }, pubSub, factory);
}

42
tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs

@ -72,7 +72,7 @@ namespace Squidex.Infrastructure.States
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IPubSub pubSub = new InMemoryPubSub(true);
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore snapshotStore = A.Fake<ISnapshotStore>();
private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly StateFactory sut;
@ -82,13 +82,15 @@ namespace Squidex.Infrastructure.States
.Returns(statefulObject);
A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot)))
.Returns(statefulObjectWithSnapShot);
A.CallTo(() => services.GetService(typeof(ISnapshotStore<int>)))
.Returns(snapshotStore);
A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key))
.Returns(key);
A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObjectWithSnapshot), key))
.Returns(key);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, snapshotStore, streamNameResolver);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver);
sut.Connect();
}
@ -102,7 +104,7 @@ namespace Squidex.Infrastructure.States
SetupEventStore(event1, event2);
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
@ -115,12 +117,12 @@ namespace Squidex.Infrastructure.States
{
statefulObjectWithSnapShot.ExpectedVersion = null;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L));
SetupEventStore(3, 2);
await sut.GetSynchronizedAsync<MyStatefulObjectWithSnapshot>(key);
await sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key);
A.CallTo(() => eventStore.GetEventsAsync(key, 3))
.MustHaveHappened();
@ -131,12 +133,12 @@ namespace Squidex.Infrastructure.States
{
statefulObjectWithSnapShot.ExpectedVersion = null;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L));
SetupEventStore(3, 0, 3);
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetSynchronizedAsync<MyStatefulObjectWithSnapshot>(key));
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key));
}
[Fact]
@ -144,12 +146,12 @@ namespace Squidex.Infrastructure.States
{
statefulObjectWithSnapShot.ExpectedVersion = null;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L));
SetupEventStore(3, 4, 3);
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetSynchronizedAsync<MyStatefulObjectWithSnapshot>(key));
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key));
}
[Fact]
@ -159,7 +161,7 @@ namespace Squidex.Infrastructure.States
SetupEventStore(0);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSynchronizedAsync<MyStatefulObject>(key));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
}
[Fact]
@ -169,7 +171,7 @@ namespace Squidex.Infrastructure.States
SetupEventStore(3);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSynchronizedAsync<MyStatefulObject>(key));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
}
[Fact]
@ -179,7 +181,7 @@ namespace Squidex.Infrastructure.States
SetupEventStore(0);
await sut.GetSynchronizedAsync<MyStatefulObject>(key);
await sut.GetSingleAsync<MyStatefulObject>(key);
}
[Fact]
@ -189,7 +191,7 @@ namespace Squidex.Infrastructure.States
SetupEventStore(0);
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
@ -202,12 +204,12 @@ namespace Squidex.Infrastructure.States
SetupEventStore(0);
var actualObject1 = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject1 = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject1);
Assert.NotNull(cache.Get<object>(key));
var actualObject2 = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject2 = await sut.GetSingleAsync<MyStatefulObject>(key);
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.MustHaveHappened(Repeated.Exactly.Once);
@ -227,7 +229,7 @@ namespace Squidex.Infrastructure.States
SetupEventStore(3);
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject);
@ -250,7 +252,7 @@ namespace Squidex.Infrastructure.States
SetupEventStore(3);
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
A.CallTo(() => eventStore.AppendEventsAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
.Throws(new WrongEventVersionException(1, 1));
@ -263,7 +265,7 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
await InvalidateCacheAsync();
@ -275,14 +277,14 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L)));
var tasks = new List<Task<MyStatefulObject>>();
for (var i = 0; i < 1000; i++)
{
tasks.Add(Task.Run(() => sut.GetSynchronizedAsync<MyStatefulObject>(key)));
tasks.Add(Task.Run(() => sut.GetSingleAsync<MyStatefulObject>(key)));
}
var retrievedStates = await Task.WhenAll(tasks);

48
tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs

@ -56,7 +56,7 @@ namespace Squidex.Infrastructure.States
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IPubSub pubSub = new InMemoryPubSub(true);
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore snapshotStore = A.Fake<ISnapshotStore>();
private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly StateFactory sut;
@ -64,8 +64,10 @@ namespace Squidex.Infrastructure.States
{
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.Returns(statefulObject);
A.CallTo(() => services.GetService(typeof(ISnapshotStore<int>)))
.Returns(snapshotStore);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, snapshotStore, streamNameResolver);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver);
sut.Connect();
}
@ -79,10 +81,10 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = 1;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, 1));
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
@ -95,10 +97,10 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = 0;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((0, -1));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSynchronizedAsync<MyStatefulObject>(key));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
}
[Fact]
@ -106,10 +108,10 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = 1;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSynchronizedAsync<MyStatefulObject>(key));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
}
[Fact]
@ -117,10 +119,10 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((0, -1));
await sut.GetSynchronizedAsync<MyStatefulObject>(key);
await sut.GetSingleAsync<MyStatefulObject>(key);
}
[Fact]
@ -128,7 +130,7 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
@ -139,12 +141,12 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
var actualObject1 = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject1 = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject1);
Assert.NotNull(cache.Get<object>(key));
var actualObject2 = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject2 = await sut.GetSingleAsync<MyStatefulObject>(key);
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.MustHaveHappened(Repeated.Exactly.Once);
@ -155,12 +157,12 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
var actualObject1 = await sut.GetDetachedAsync<MyStatefulObject>(key);
var actualObject1 = await sut.CreateAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject1);
Assert.Null(cache.Get<object>(key));
var actualObject2 = await sut.GetDetachedAsync<MyStatefulObject>(key);
var actualObject2 = await sut.CreateAsync<MyStatefulObject>(key);
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.MustHaveHappened(Repeated.Exactly.Twice);
@ -180,10 +182,10 @@ namespace Squidex.Infrastructure.States
message = m;
});
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, version));
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
Assert.Same(statefulObject, actualObject);
Assert.Equal(123, statefulObject.State);
@ -206,13 +208,13 @@ namespace Squidex.Infrastructure.States
var version = 1;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, version));
A.CallTo(() => snapshotStore.WriteAsync(key, 123, version, 2))
.Throws(new InconsistentStateException(1, 1, new InvalidOperationException()));
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => statefulObject.WriteStateAsync());
}
@ -222,7 +224,7 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
var actualObject = await sut.GetSynchronizedAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
await InvalidateCacheAsync();
@ -234,14 +236,14 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = null;
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L)));
var tasks = new List<Task<MyStatefulObject>>();
for (var i = 0; i < 1000; i++)
{
tasks.Add(Task.Run(() => sut.GetSynchronizedAsync<MyStatefulObject>(key)));
tasks.Add(Task.Run(() => sut.GetSingleAsync<MyStatefulObject>(key)));
}
var retrievedStates = await Task.WhenAll(tasks);
@ -251,7 +253,7 @@ namespace Squidex.Infrastructure.States
Assert.Same(retrievedStates[0], retrievedState);
}
A.CallTo(() => snapshotStore.ReadAsync<int>(key))
A.CallTo(() => snapshotStore.ReadAsync(key))
.MustHaveHappened(Repeated.Exactly.Once);
}

Loading…
Cancel
Save