mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
409 changed files with 4694 additions and 5421 deletions
@ -0,0 +1,41 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoAppEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.State; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Apps |
||||
|
{ |
||||
|
public sealed class MongoAppEntity |
||||
|
{ |
||||
|
[BsonId] |
||||
|
[BsonElement] |
||||
|
[BsonRepresentation(BsonType.String)] |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public int Version { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public string[] UserIds { get; set; } |
||||
|
|
||||
|
[BsonJson] |
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public AppState State { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoAppRepository.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.Apps.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.State; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Apps |
||||
|
{ |
||||
|
public sealed class MongoAppRepository : MongoRepositoryBase<MongoAppEntity>, IAppRepository, ISnapshotStore<AppState, Guid> |
||||
|
{ |
||||
|
public MongoAppRepository(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "States_Apps"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoAppEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); |
||||
|
} |
||||
|
|
||||
|
public async Task<Guid> FindAppIdByNameAsync(string name) |
||||
|
{ |
||||
|
var appEntity = |
||||
|
await Collection.Find(x => x.Name == name).Only(x => x.Id) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
return appEntity != null ? Guid.Parse(appEntity["_id"].AsString) : 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"].AsString)).ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task<(AppState Value, long Version)> ReadAsync(Guid key) |
||||
|
{ |
||||
|
var existing = |
||||
|
await Collection.Find(x => x.Id == key) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existing != null) |
||||
|
{ |
||||
|
return (existing.State, existing.Version); |
||||
|
} |
||||
|
|
||||
|
return (null, EtagVersion.NotFound); |
||||
|
} |
||||
|
|
||||
|
public async Task WriteAsync(Guid key, AppState value, long oldVersion, long newVersion) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, |
||||
|
Update |
||||
|
.Set(x => x.UserIds, value.Contributors.Keys.ToArray()) |
||||
|
.Set(x => x.Name, value.Name) |
||||
|
.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) |
||||
|
.Project<MongoAppEntity>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existingVersion != null) |
||||
|
{ |
||||
|
throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoAssetEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.State; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets |
||||
|
{ |
||||
|
public sealed class MongoAssetEntity |
||||
|
{ |
||||
|
[BsonId] |
||||
|
[BsonElement] |
||||
|
[BsonRepresentation(BsonType.String)] |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public AssetState State { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public int Version { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,248 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoContentRepository.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 Microsoft.OData.UriParser; |
||||
|
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 partial class MongoContentRepository : MongoRepositoryBase<MongoContentEntity>, |
||||
|
IEventConsumer, |
||||
|
IContentRepository, |
||||
|
ISnapshotStore<ContentState, Guid> |
||||
|
{ |
||||
|
private readonly IAppProvider appProvider; |
||||
|
|
||||
|
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) |
||||
|
: base(database) |
||||
|
{ |
||||
|
Guard.NotNull(appProvider, nameof(appProvider)); |
||||
|
|
||||
|
this.appProvider = appProvider; |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "States_Contents"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync( |
||||
|
Index |
||||
|
.Ascending(x => x.Id) |
||||
|
.Ascending(x => x.Version)); |
||||
|
|
||||
|
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)); |
||||
|
} |
||||
|
|
||||
|
public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) |
||||
|
{ |
||||
|
var documentId = $"{key}_{newVersion}"; |
||||
|
|
||||
|
var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId, true); |
||||
|
|
||||
|
if (schema == null) |
||||
|
{ |
||||
|
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, |
||||
|
IsLatest = true, |
||||
|
ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef), |
||||
|
}); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await Collection.InsertOneAsync(document); |
||||
|
await Collection.UpdateManyAsync(x => x.Id == value.Id && x.Version < value.Version, Update.Set(x => x.IsLatest, false)); |
||||
|
} |
||||
|
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"].AsInt64, oldVersion, ex); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key) |
||||
|
{ |
||||
|
var contentEntity = |
||||
|
await Collection.Find(x => x.Id == key && x.IsLatest) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (contentEntity != null) |
||||
|
{ |
||||
|
var schema = await appProvider.GetSchemaAsync(contentEntity.AppId, contentEntity.SchemaId, true); |
||||
|
|
||||
|
if (schema == null) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"Cannot find schema {contentEntity.SchemaId}"); |
||||
|
} |
||||
|
|
||||
|
contentEntity?.ParseData(schema.SchemaDef); |
||||
|
|
||||
|
return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); |
||||
|
} |
||||
|
|
||||
|
return (null, EtagVersion.NotFound); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) |
||||
|
{ |
||||
|
IFindFluent<MongoContentEntity, MongoContentEntity> cursor; |
||||
|
try |
||||
|
{ |
||||
|
cursor = |
||||
|
Collection |
||||
|
.Find(odataQuery, schema.Id, schema.SchemaDef, status) |
||||
|
.Take(odataQuery) |
||||
|
.Skip(odataQuery) |
||||
|
.Sort(odataQuery, schema.SchemaDef); |
||||
|
} |
||||
|
catch (NotSupportedException) |
||||
|
{ |
||||
|
throw new ValidationException("This odata operation is not supported."); |
||||
|
} |
||||
|
catch (NotImplementedException) |
||||
|
{ |
||||
|
throw new ValidationException("This odata operation is not supported."); |
||||
|
} |
||||
|
|
||||
|
var contentEntities = await cursor.ToListAsync(); |
||||
|
|
||||
|
foreach (var entity in contentEntities) |
||||
|
{ |
||||
|
entity.ParseData(schema.SchemaDef); |
||||
|
} |
||||
|
|
||||
|
return contentEntities; |
||||
|
} |
||||
|
|
||||
|
public async Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) |
||||
|
{ |
||||
|
IFindFluent<MongoContentEntity, MongoContentEntity> cursor; |
||||
|
try |
||||
|
{ |
||||
|
cursor = Collection.Find(odataQuery, schema.Id, schema.SchemaDef, status); |
||||
|
} |
||||
|
catch (NotSupportedException) |
||||
|
{ |
||||
|
throw new ValidationException("This odata operation is not supported."); |
||||
|
} |
||||
|
catch (NotImplementedException) |
||||
|
{ |
||||
|
throw new ValidationException("This odata operation is not supported."); |
||||
|
} |
||||
|
|
||||
|
return await cursor.CountAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids) |
||||
|
{ |
||||
|
var contentsCount = |
||||
|
await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest) |
||||
|
.CountAsync(); |
||||
|
|
||||
|
return contentsCount; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids) |
||||
|
{ |
||||
|
var contentEntities = |
||||
|
await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
foreach (var entity in contentEntities) |
||||
|
{ |
||||
|
entity.ParseData(schema.SchemaDef); |
||||
|
} |
||||
|
|
||||
|
return contentEntities.OfType<IContentEntity>().ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds) |
||||
|
{ |
||||
|
var contentEntities = |
||||
|
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(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version) |
||||
|
{ |
||||
|
var contentEntity = |
||||
|
await Collection.Find(x => x.Id == id && x.Version >= version).SortBy(x => x.Version) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
contentEntity?.ParseData(schema.SchemaDef); |
||||
|
|
||||
|
return contentEntity; |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) |
||||
|
{ |
||||
|
var contentEntity = |
||||
|
await Collection.Find(x => x.Id == id && x.IsLatest) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
contentEntity?.ParseData(schema.SchemaDef); |
||||
|
|
||||
|
return contentEntity; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoRuleEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.State; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Rules |
||||
|
{ |
||||
|
public sealed class MongoRuleEntity |
||||
|
{ |
||||
|
[BsonId] |
||||
|
[BsonElement] |
||||
|
[BsonRepresentation(BsonType.String)] |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public int Version { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public bool IsDeleted { get; set; } |
||||
|
|
||||
|
[BsonJson] |
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public RuleState State { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoRuleRepository.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.Rules.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.State; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Rules |
||||
|
{ |
||||
|
public sealed class MongoRuleRepository : MongoRepositoryBase<MongoRuleEntity>, IRuleRepository, ISnapshotStore<RuleState, Guid> |
||||
|
{ |
||||
|
public MongoRuleRepository(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "States_Rules"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoRuleEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)); |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); |
||||
|
} |
||||
|
|
||||
|
public async Task<(RuleState Value, long Version)> ReadAsync(Guid key) |
||||
|
{ |
||||
|
var existing = |
||||
|
await Collection.Find(x => x.Id == key) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existing != null) |
||||
|
{ |
||||
|
return (existing.State, existing.Version); |
||||
|
} |
||||
|
|
||||
|
return (null, EtagVersion.NotFound); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<Guid>> QueryRuleIdsAsync(Guid appId) |
||||
|
{ |
||||
|
var ruleEntities = |
||||
|
await Collection.Find(x => x.AppId == appId && !x.IsDeleted).Only(x => x.Id) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
return ruleEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task WriteAsync(Guid key, RuleState 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.AppId, value.AppId) |
||||
|
.Set(x => x.IsDeleted, value.IsDeleted) |
||||
|
.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.Id, x => x.Version) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existingVersion != null) |
||||
|
{ |
||||
|
throw new InconsistentStateException(existingVersion["Version"].AsInt64, oldVersion, ex); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoSchemaEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas.State; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas |
||||
|
{ |
||||
|
public sealed class MongoSchemaEntity |
||||
|
{ |
||||
|
[BsonId] |
||||
|
[BsonElement] |
||||
|
[BsonRepresentation(BsonType.String)] |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public int Version { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
[BsonJson] |
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public SchemaState State { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas |
||||
|
{ |
||||
|
public sealed class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, ISnapshotStore<SchemaState, Guid> |
||||
|
{ |
||||
|
public MongoSchemaRepository(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "States_Schemas"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoSchemaEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)); |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); |
||||
|
} |
||||
|
|
||||
|
public async Task<(SchemaState Value, long Version)> ReadAsync(Guid key) |
||||
|
{ |
||||
|
var existing = |
||||
|
await Collection.Find(x => x.Id == key) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existing != null) |
||||
|
{ |
||||
|
return (existing.State, existing.Version); |
||||
|
} |
||||
|
|
||||
|
return (null, EtagVersion.NotFound); |
||||
|
} |
||||
|
|
||||
|
public async Task<Guid> FindSchemaIdAsync(Guid appId, string name) |
||||
|
{ |
||||
|
var schemaEntity = |
||||
|
await Collection.Find(x => x.Name == name).Only(x => x.Id) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
return schemaEntity != null ? Guid.Parse(schemaEntity["_id"].AsString) : Guid.Empty; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<Guid>> QuerySchemaIdsAsync(Guid appId) |
||||
|
{ |
||||
|
var schemaEntities = |
||||
|
await Collection.Find(x => x.AppId == appId).Only(x => x.Id) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task WriteAsync(Guid 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.AppId, value.AppId) |
||||
|
.Set(x => x.Name, value.Name) |
||||
|
.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"].AsInt64, oldVersion, ex); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,150 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppProvider.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.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; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities |
||||
|
{ |
||||
|
public sealed class AppProvider : IAppProvider |
||||
|
{ |
||||
|
private readonly IAppRepository appRepository; |
||||
|
private readonly IRuleRepository ruleRepository; |
||||
|
private readonly ISchemaRepository schemaRepository; |
||||
|
private readonly IStateFactory stateFactory; |
||||
|
|
||||
|
public AppProvider( |
||||
|
IAppRepository appRepository, |
||||
|
ISchemaRepository schemaRepository, |
||||
|
IStateFactory stateFactory, |
||||
|
IRuleRepository ruleRepository) |
||||
|
{ |
||||
|
Guard.NotNull(appRepository, nameof(appRepository)); |
||||
|
Guard.NotNull(schemaRepository, nameof(schemaRepository)); |
||||
|
Guard.NotNull(stateFactory, nameof(stateFactory)); |
||||
|
Guard.NotNull(ruleRepository, nameof(ruleRepository)); |
||||
|
|
||||
|
this.appRepository = appRepository; |
||||
|
this.schemaRepository = schemaRepository; |
||||
|
this.stateFactory = stateFactory; |
||||
|
this.ruleRepository = ruleRepository; |
||||
|
} |
||||
|
|
||||
|
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) |
||||
|
{ |
||||
|
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId); |
||||
|
|
||||
|
if (IsNotFound(app)) |
||||
|
{ |
||||
|
return (null, null); |
||||
|
} |
||||
|
|
||||
|
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id); |
||||
|
|
||||
|
return IsNotFound(false, schema) ? (null, null) : (app.State, schema.State); |
||||
|
} |
||||
|
|
||||
|
public async Task<IAppEntity> GetAppAsync(string appName) |
||||
|
{ |
||||
|
var appId = await GetAppIdAsync(appName); |
||||
|
|
||||
|
if (appId == Guid.Empty) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId); |
||||
|
|
||||
|
return IsNotFound(app) ? null : app.State; |
||||
|
} |
||||
|
|
||||
|
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name, bool provideDeleted = false) |
||||
|
{ |
||||
|
var schemaId = await GetSchemaIdAsync(appId, name); |
||||
|
|
||||
|
if (schemaId == Guid.Empty) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaId); |
||||
|
|
||||
|
return IsNotFound(provideDeleted, schema) ? null : schema.State; |
||||
|
} |
||||
|
|
||||
|
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false) |
||||
|
{ |
||||
|
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id); |
||||
|
|
||||
|
return IsNotFound(provideDeleted, schema) ? null : schema.State; |
||||
|
} |
||||
|
|
||||
|
public async Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId) |
||||
|
{ |
||||
|
var ids = await schemaRepository.QuerySchemaIdsAsync(appId); |
||||
|
|
||||
|
var schemas = |
||||
|
await Task.WhenAll( |
||||
|
ids.Select(id => stateFactory.GetSingleAsync<SchemaDomainObject>(id))); |
||||
|
|
||||
|
return schemas.Select(a => (ISchemaEntity)a.State).ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<IRuleEntity>> GetRulesAsync(Guid appId) |
||||
|
{ |
||||
|
var ids = await ruleRepository.QueryRuleIdsAsync(appId); |
||||
|
|
||||
|
var rules = |
||||
|
await Task.WhenAll( |
||||
|
ids.Select(id => stateFactory.GetSingleAsync<RuleDomainObject>(id))); |
||||
|
|
||||
|
return rules.Select(a => (IRuleEntity)a.State).ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<IAppEntity>> GetUserApps(string userId) |
||||
|
{ |
||||
|
var ids = await appRepository.QueryUserAppIdsAsync(userId); |
||||
|
|
||||
|
var apps = |
||||
|
await Task.WhenAll( |
||||
|
ids.Select(id => stateFactory.GetSingleAsync<AppDomainObject>(id))); |
||||
|
|
||||
|
return apps.Select(a => (IAppEntity)a.State).ToList(); |
||||
|
} |
||||
|
|
||||
|
private Task<Guid> GetAppIdAsync(string name) |
||||
|
{ |
||||
|
return appRepository.FindAppIdByNameAsync(name); |
||||
|
} |
||||
|
|
||||
|
private Task<Guid> GetSchemaIdAsync(Guid appId, string name) |
||||
|
{ |
||||
|
return schemaRepository.FindSchemaIdAsync(appId, name); |
||||
|
} |
||||
|
|
||||
|
private static bool IsNotFound(AppDomainObject app) |
||||
|
{ |
||||
|
return app.Version < 0; |
||||
|
} |
||||
|
|
||||
|
private static bool IsNotFound(bool provideDeleted, SchemaDomainObject schema) |
||||
|
{ |
||||
|
return schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IAppRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Repositories |
||||
|
{ |
||||
|
public interface IAppRepository |
||||
|
{ |
||||
|
Task<Guid> FindAppIdByNameAsync(string name); |
||||
|
|
||||
|
Task<IReadOnlyList<Guid>> QueryUserAppIdsAsync(string userId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,107 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppState.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Dispatching; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.State |
||||
|
{ |
||||
|
public class AppState : DomainObjectState<AppState>, |
||||
|
IAppEntity |
||||
|
{ |
||||
|
private static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN); |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public AppPlan Plan { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public AppClients Clients { get; set; } = AppClients.Empty; |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public AppContributors Contributors { get; set; } = AppContributors.Empty; |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public LanguagesConfig LanguagesConfig { get; set; } = English; |
||||
|
|
||||
|
protected void On(AppCreated @event) |
||||
|
{ |
||||
|
SimpleMapper.Map(@event, this); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppPlanChanged @event) |
||||
|
{ |
||||
|
Plan = @event.PlanId == null ? null : new AppPlan(@event.Actor, @event.PlanId); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppContributorAssigned @event) |
||||
|
{ |
||||
|
Contributors = Contributors.Assign(@event.ContributorId, @event.Permission); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppContributorRemoved @event) |
||||
|
{ |
||||
|
Contributors = Contributors.Remove(@event.ContributorId); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppClientAttached @event) |
||||
|
{ |
||||
|
Clients = Clients.Add(@event.Id, @event.Secret); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppClientUpdated @event) |
||||
|
{ |
||||
|
Clients = Clients.Update(@event.Id, @event.Permission); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppClientRenamed @event) |
||||
|
{ |
||||
|
Clients = Clients.Rename(@event.Id, @event.Name); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppClientRevoked @event) |
||||
|
{ |
||||
|
Clients = Clients.Revoke(@event.Id); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppLanguageAdded @event) |
||||
|
{ |
||||
|
LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language)); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppLanguageRemoved @event) |
||||
|
{ |
||||
|
LanguagesConfig = LanguagesConfig.Remove(@event.Language); |
||||
|
} |
||||
|
|
||||
|
protected void On(AppLanguageUpdated @event) |
||||
|
{ |
||||
|
LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, @event.Fallback)); |
||||
|
|
||||
|
if (@event.IsMaster) |
||||
|
{ |
||||
|
LanguagesConfig = LanguagesConfig.MakeMaster(@event.Language); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public AppState Apply(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
var payload = (SquidexEvent)@event.Payload; |
||||
|
|
||||
|
return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AssetState.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Domain.Apps.Core.ValidateContent; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Assets; |
||||
|
using Squidex.Infrastructure.Dispatching; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.State |
||||
|
{ |
||||
|
public class AssetState : DomainObjectState<AssetState>, |
||||
|
IAssetEntity, |
||||
|
IAssetInfo, |
||||
|
IUpdateableEntityWithAppRef |
||||
|
{ |
||||
|
[JsonProperty] |
||||
|
public Guid AppId { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public string FileName { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public string MimeType { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public long FileVersion { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public long FileSize { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public long TotalSize { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public bool IsImage { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public int? PixelWidth { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public int? PixelHeight { get; set; } |
||||
|
|
||||
|
[JsonProperty] |
||||
|
public bool IsDeleted { get; set; } |
||||
|
|
||||
|
Guid IAssetInfo.AssetId |
||||
|
{ |
||||
|
get { return Id; } |
||||
|
} |
||||
|
|
||||
|
protected void On(AssetCreated @event) |
||||
|
{ |
||||
|
SimpleMapper.Map(@event, this); |
||||
|
|
||||
|
TotalSize += @event.FileSize; |
||||
|
} |
||||
|
|
||||
|
protected void On(AssetUpdated @event) |
||||
|
{ |
||||
|
SimpleMapper.Map(@event, this); |
||||
|
|
||||
|
TotalSize += @event.FileSize; |
||||
|
} |
||||
|
|
||||
|
protected void On(AssetRenamed @event) |
||||
|
{ |
||||
|
FileName = @event.FileName; |
||||
|
} |
||||
|
|
||||
|
protected void On(AssetDeleted @event) |
||||
|
{ |
||||
|
IsDeleted = true; |
||||
|
} |
||||
|
|
||||
|
public AssetState Apply(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
var payload = (SquidexEvent)@event.Payload; |
||||
|
|
||||
|
return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue