Browse Source

A lot progress

pull/1/head
Sebastian Stehle 9 years ago
parent
commit
b8d6590e8b
  1. 13
      src/PinkParrot/Configurations/InfrastructureModule.cs
  2. 3
      src/PinkParrot/Configurations/ReadModule.cs
  3. 17
      src/PinkParrot/Configurations/Serializers.cs
  4. 23
      src/PinkParrot/Modules/Api/BaseController.cs
  5. 2
      src/PinkParrot/Modules/Api/Schemas/SchemaFieldsController.cs
  6. 39
      src/PinkParrot/Modules/Api/Schemas/SchemasController.cs
  7. 2
      src/PinkParrot/Modules/Api/Schemas/SchemasDto.cs
  8. 40
      src/PinkParrot/Modules/ControllerBase.cs
  9. 31
      src/PinkParrot/Pipeline/CommandHandlers/EnrichWithAggregateIdHandler.cs
  10. 20
      src/PinkParrot/Pipeline/CommandHandlers/EnrichWithTenantIdHandler.cs
  11. 17
      src/PinkParrot/Pipeline/ITenantFeature.cs
  12. 45
      src/PinkParrot/Pipeline/TenantMiddleware.cs
  13. 4
      src/PinkParrot/Startup.cs
  14. 68
      src/pinkparrot_core/PinkParrot.Core/Schema/Json/SchemaDto.cs
  15. 20
      src/pinkparrot_core/PinkParrot.Core/Schema/ModelField.cs
  16. 9
      src/pinkparrot_core/PinkParrot.Core/Schema/ModelFieldFactory.cs
  17. 5
      src/pinkparrot_core/PinkParrot.Core/Schema/ModelField_Generic.cs
  18. 57
      src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchema.cs
  19. 4
      src/pinkparrot_core/PinkParrot.Core/Schema/NumberField.cs
  20. 27
      src/pinkparrot_core/PinkParrot.Core/Schema/NumberFieldProperties.cs
  21. 2
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs
  22. 5
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Commands/InMemoryCommandBus.cs
  23. 15
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/DomainObject.cs
  24. 9
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/DefaultNameResolver.cs
  25. 101
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreBus.cs
  26. 2
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs
  27. 6
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs
  28. 5
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Events/IEventConsumer.cs
  29. 68
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/ReflectionExtensions.cs
  30. 4
      src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/SimpleMapper.cs
  31. 14
      src/pinkparrot_read/PinkParrot.Read/IEntity.cs
  32. 11
      src/pinkparrot_read/PinkParrot.Read/IModelSchemaRM.cs
  33. 26
      src/pinkparrot_read/PinkParrot.Read/Repositories/EntityWithSchema.cs
  34. 2
      src/pinkparrot_read/PinkParrot.Read/Repositories/IEntity.cs
  35. 14
      src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaEntity.cs
  36. 9
      src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaRepository.cs
  37. 4
      src/pinkparrot_read/PinkParrot.Read/Repositories/ITenantEntity.cs
  38. 19
      src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/EntityMapper.cs
  39. 11
      src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaEntity.cs
  40. 66
      src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaListRepository.cs
  41. 172
      src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaRepository.cs
  42. 2
      src/pinkparrot_read/PinkParrot.Read/Services/IModelSchemaProvider.cs
  43. 2
      src/pinkparrot_read/PinkParrot.Read/Services/ITenantProvider.cs
  44. 69
      src/pinkparrot_read/PinkParrot.Read/Services/Implementations/ModelSchemaProvider.cs
  45. 5
      src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoPositions.cs
  46. 31
      src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoStreamPositionsStorage.cs
  47. 4
      src/pinkparrot_read/PinkParrot.Read/Services/Implementations/TenantProvider.cs
  48. 1
      src/pinkparrot_read/PinkParrot.Read/project.json
  49. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/AddModelField.cs
  50. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/DeleteModelField.cs
  51. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/DeleteModelSchema.cs
  52. 4
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/DisableModelField.cs
  53. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/EnableModelField.cs
  54. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/HideModelField.cs
  55. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/ShowModelField.cs
  56. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/UpdateModelField.cs
  57. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/Commands/UpdateModelSchema.cs
  58. 2
      src/pinkparrot_write/PinkParrot.Write/Schema/ModelSchemaCommandHandler.cs
  59. 25
      src/pinkparrot_write/PinkParrot.Write/Schema/ModelSchemaDomainObject.cs
  60. 2
      src/pinkparrot_write/PinkParrot.Write/TenantCommand.cs

13
src/PinkParrot/Configurations/InfrastructureModule.cs

@ -1,5 +1,5 @@
// ==========================================================================
// InfrastructureDependencies.cs
// InfrastructureModule.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
@ -13,10 +13,12 @@ using EventStore.ClientAPI.SystemData;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using PinkParrot.Infrastructure.CQRS.Autofac;
using PinkParrot.Infrastructure.CQRS.Commands;
using PinkParrot.Infrastructure.CQRS.EventStore;
using PinkParrot.Pipeline;
using PinkParrot.Read.Services.Implementations;
namespace PinkParrot.Configurations
@ -63,7 +65,7 @@ namespace PinkParrot.Configurations
.As<IDomainObjectFactory>()
.SingleInstance();
builder.RegisterType<DefaultNameResolver>()
builder.RegisterInstance(new DefaultNameResolver("pinkparrot"))
.As<IStreamNameResolver>()
.SingleInstance();
@ -89,7 +91,12 @@ namespace PinkParrot.Configurations
{
public static void UseAppEventBus(this IApplicationBuilder app)
{
app.ApplicationServices.GetService(typeof(EventStoreBus));
app.ApplicationServices.GetService<EventStoreBus>().Subscribe("pinkparrot");
}
public static void UseAppTenants(this IApplicationBuilder app)
{
app.UseMiddleware<TenantMiddleware>();
}
}
}

3
src/PinkParrot/Configurations/ReadModule.cs

@ -25,9 +25,10 @@ namespace PinkParrot.Configurations
builder.RegisterType<ModelSchemaProvider>()
.As<IModelSchemaProvider>()
.As<ILiveEventConsumer>()
.SingleInstance();
builder.RegisterType<MongoModelSchemaListRepository>()
builder.RegisterType<MongoModelSchemaRepository>()
.As<IModelSchemaRepository>()
.As<ICatchEventConsumer>()
.SingleInstance();

17
src/PinkParrot/Configurations/Serializers.cs

@ -7,15 +7,12 @@
// ==========================================================================
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using PinkParrot.Core.Schema;
using PinkParrot.Infrastructure.CQRS.EventStore;
using PinkParrot.Infrastructure.Json;
using IMvcBuilder = Microsoft.Extensions.DependencyInjection.IMvcBuilder;
using IServiceCollection = Microsoft.Extensions.DependencyInjection.IServiceCollection;
using MvcJsonMvcBuilderExtensions = Microsoft.Extensions.DependencyInjection.MvcJsonMvcBuilderExtensions;
using ServiceCollectionServiceExtensions = Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions;
namespace PinkParrot.Configurations
{
@ -40,18 +37,16 @@ namespace PinkParrot.Configurations
public static void AddEventFormatter(this IServiceCollection services)
{
var fieldFactory =
new ModelFieldFactory()
.AddFactory<NumberFieldProperties>(id => new NumberField(id));
var fieldFactory = new ModelFieldFactory();
ServiceCollectionServiceExtensions.AddSingleton(services, t => CreateSettings());
ServiceCollectionServiceExtensions.AddSingleton(services, fieldFactory);
ServiceCollectionServiceExtensions.AddSingleton<EventStoreFormatter>(services);
services.AddSingleton(t => CreateSettings());
services.AddSingleton(fieldFactory);
services.AddSingleton<EventStoreFormatter>();
}
public static void AddAppSerializers(this IMvcBuilder mvc)
{
MvcJsonMvcBuilderExtensions.AddJsonOptions(mvc, options =>
mvc.AddJsonOptions(options =>
{
ConfigureJson(options.SerializerSettings);
});

23
src/PinkParrot/Modules/Api/BaseController.cs

@ -1,23 +0,0 @@
// ==========================================================================
// BaseController.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Modules.Api
{
public abstract class BaseController : Controller
{
public ICommandBus CommandBus { get; }
protected BaseController(ICommandBus commandBus)
{
CommandBus = commandBus;
}
}
}

2
src/PinkParrot/Modules/Api/Schemas/SchemaFieldsController.cs

@ -17,7 +17,7 @@ using Swashbuckle.SwaggerGen.Annotations;
namespace PinkParrot.Modules.Api.Schemas
{
public class SchemasFieldsController : BaseController
public class SchemasFieldsController : ControllerBase
{
public SchemasFieldsController(ICommandBus commandBus)
: base(commandBus)

39
src/PinkParrot/Modules/Api/Schemas/SchemasController.cs

@ -12,10 +12,10 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using PinkParrot.Core.Schema;
using PinkParrot.Core.Schema.Json;
using PinkParrot.Infrastructure.CQRS.Commands;
using PinkParrot.Infrastructure.Reflection;
using PinkParrot.Read.Repositories;
using PinkParrot.Read.Services;
using PinkParrot.Write.Schema.Commands;
using Swashbuckle.SwaggerGen.Annotations;
@ -23,17 +23,14 @@ using Swashbuckle.SwaggerGen.Annotations;
namespace PinkParrot.Modules.Api.Schemas
{
public class SchemasController : BaseController
public class SchemasController : ControllerBase
{
private readonly IModelSchemaRepository modelSchemaRepository;
private readonly ITenantProvider tenantProvider;
public SchemasController(ICommandBus commandBus, ITenantProvider tenantProvider, IModelSchemaRepository modelSchemaRepository)
public SchemasController(ICommandBus commandBus, IModelSchemaRepository modelSchemaRepository)
: base(commandBus)
{
this.modelSchemaRepository = modelSchemaRepository;
this.tenantProvider = tenantProvider;
}
/// <summary>
@ -42,12 +39,34 @@ namespace PinkParrot.Modules.Api.Schemas
[HttpGet]
[Route("schemas/")]
[SwaggerOperation(Tags = new[] { "Schemas" })]
public async Task<List<SchemaListModel>> Query()
[ProducesResponseType(typeof(List<SchemasDto>), 200)]
public async Task<List<SchemasDto>> Query()
{
var tenantId = await tenantProvider.ProvideTenantIdByDomainAsync(Request.Host.ToString());
var schemes = await modelSchemaRepository.QueryAllAsync(tenantId);
var schemas = await modelSchemaRepository.QueryAllAsync(TenantId);
return schemas.Select(s => SimpleMapper.Map(s, new SchemasDto())).ToList();
}
/// <summary>
/// Gets the schema with the specified name.
/// </summary>
/// <param name="name">The name of the schema.</param>
/// <response code="200">Schema returned</response>
/// <response code="404">Schema not found</response>
[HttpGet]
[Route("schemas/{name}/")]
[SwaggerOperation(Tags = new[] { "Schemas" })]
[ProducesResponseType(typeof(SchemaDto), 200)]
public async Task<ActionResult> Get(string name)
{
var entity = await modelSchemaRepository.FindSchemaAsync(TenantId, name);
if (entity == null)
{
return NotFound();
}
return schemes.Select(s => SimpleMapper.Map(s, new SchemaListModel())).ToList();
return Ok(SchemaDto.Create(entity.Schema));
}
/// <summary>

2
src/PinkParrot/Modules/Api/Schemas/SchemaListModel.cs → src/PinkParrot/Modules/Api/Schemas/SchemasDto.cs

@ -11,7 +11,7 @@ using System.ComponentModel.DataAnnotations;
namespace PinkParrot.Modules.Api.Schemas
{
public class SchemaListModel
public class SchemasDto
{
[Required]
public Guid Id { get; set; }

40
src/PinkParrot/Modules/ControllerBase.cs

@ -0,0 +1,40 @@
// ==========================================================================
// ControllerBase.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.AspNetCore.Mvc;
using PinkParrot.Infrastructure.CQRS.Commands;
using PinkParrot.Pipeline;
namespace PinkParrot.Modules
{
public abstract class ControllerBase : Controller
{
public ICommandBus CommandBus { get; }
protected ControllerBase(ICommandBus commandBus)
{
CommandBus = commandBus;
}
public Guid TenantId
{
get
{
var tenantFeature = HttpContext.Features.Get<ITenantFeature>();
if (tenantFeature == null)
{
throw new InvalidOperationException("Not in a tenant context");
}
return tenantFeature.TenantId;
}
}
}
}

31
src/PinkParrot/Pipeline/CommandHandlers/EnrichWithAggregateIdHandler.cs

@ -9,8 +9,11 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using PinkParrot.Infrastructure;
using PinkParrot.Infrastructure.CQRS.Commands;
using PinkParrot.Read.Services;
using PinkParrot.Write;
using PinkParrot.Write.Schema;
// ReSharper disable InvertIf
@ -32,16 +35,32 @@ namespace PinkParrot.Pipeline.CommandHandlers
{
var aggregateCommand = context.Command as IAggregateCommand;
if (aggregateCommand != null && aggregateCommand.AggregateId == Guid.Empty)
if (aggregateCommand == null || aggregateCommand.AggregateId != Guid.Empty)
{
var routeValues = actionContextAccessor.ActionContext.RouteData.Values;
return false;
}
if (routeValues.ContainsKey("name"))
{
var schemeName = routeValues["name"];
var tenantCommand = context.Command as ITenantCommand;
if (tenantCommand == null)
{
return false;
}
var routeValues = actionContextAccessor.ActionContext.RouteData.Values;
aggregateCommand.AggregateId = await modelSchemaProvider.FindSchemaIdByNameAsync(schemeName.ToString());
if (routeValues.ContainsKey("name"))
{
var schemaName = routeValues["name"].ToString();
var id = await modelSchemaProvider.FindSchemaIdByNameAsync(tenantCommand.TenantId, schemaName);
if (!id.HasValue)
{
throw new DomainObjectNotFoundException(schemaName, typeof(ModelSchemaDomainObject));
}
aggregateCommand.AggregateId = id.Value;
}
return false;

20
src/PinkParrot/Pipeline/CommandHandlers/EnrichWithTenantIdHandler.cs

@ -6,10 +6,10 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using PinkParrot.Infrastructure.CQRS.Commands;
using PinkParrot.Read.Services;
using PinkParrot.Write;
// ReSharper disable InvertIf
@ -18,28 +18,30 @@ namespace PinkParrot.Pipeline.CommandHandlers
{
public sealed class EnrichWithTenantIdHandler : ICommandHandler
{
private readonly ITenantProvider tenantProvider;
private readonly IHttpContextAccessor httpContextAccessor;
public EnrichWithTenantIdHandler(ITenantProvider tenantProvider, IHttpContextAccessor httpContextAccessor)
public EnrichWithTenantIdHandler(IHttpContextAccessor httpContextAccessor)
{
this.tenantProvider = tenantProvider;
this.httpContextAccessor = httpContextAccessor;
}
public async Task<bool> HandleAsync(CommandContext context)
public Task<bool> HandleAsync(CommandContext context)
{
var tenantCommand = context.Command as ITenantCommand;
if (tenantCommand != null)
{
var domain = httpContextAccessor.HttpContext.Request.Host.ToString();
var tenantFeature = httpContextAccessor.HttpContext.Features.Get<ITenantFeature>();
if (tenantFeature == null)
{
throw new InvalidOperationException("Cannot reslolve tenant");
}
tenantCommand.TenantId = await tenantProvider.ProvideTenantIdByDomainAsync(domain);
tenantCommand.TenantId = tenantFeature.TenantId;
}
return false;
return Task.FromResult(false);
}
}
}

17
src/PinkParrot/Pipeline/ITenantFeature.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ITenantFeature.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System;
namespace PinkParrot.Pipeline
{
public interface ITenantFeature
{
Guid TenantId { get; }
}
}

45
src/PinkParrot/Pipeline/TenantMiddleware.cs

@ -0,0 +1,45 @@
// ==========================================================================
// TenantMiddleware.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using PinkParrot.Read.Services;
namespace PinkParrot.Pipeline
{
public sealed class TenantMiddleware
{
private readonly ITenantProvider tenantProvider;
private readonly RequestDelegate next;
public TenantMiddleware(RequestDelegate next, ITenantProvider tenantProvider)
{
this.next = next;
this.tenantProvider = tenantProvider;
}
private class TenantFeature : ITenantFeature
{
public Guid TenantId { get; set; }
}
public async Task Invoke(HttpContext context)
{
var tenantId = await tenantProvider.ProvideTenantIdByDomainAsync(context.Request.Host.ToString());
if (tenantId.HasValue)
{
context.Features.Set<ITenantFeature>(new TenantFeature { TenantId = tenantId.Value });
}
await next(context);
}
}
}

4
src/PinkParrot/Startup.cs

@ -25,14 +25,15 @@ namespace PinkParrot
{
services.AddMvc().AddAppSerializers();
services.AddRouting();
services.AddMemoryCache();
services.AddAppSwagger();
services.AddEventFormatter();
var builder = new ContainerBuilder();
builder.Populate(services);
builder.RegisterModule<InfrastructureModule>();
builder.RegisterModule<ReadModule>();
builder.RegisterModule<WriteModule>();
builder.Populate(services);
return new AutofacServiceProvider(builder.Build());
}
@ -41,6 +42,7 @@ namespace PinkParrot
{
loggerFactory.AddConsole();
app.UseAppTenants();
app.UseMvc();
app.UseStaticFiles();
app.UseAppSwagger();

68
src/pinkparrot_core/PinkParrot.Core/Schema/Json/SchemaDto.cs

@ -0,0 +1,68 @@
// ==========================================================================
// SerializationModel.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using PinkParrot.Infrastructure;
// ReSharper disable LoopCanBeConvertedToQuery
namespace PinkParrot.Core.Schema.Json
{
public class SchemaDto
{
private readonly ModelSchemaProperties properties;
private readonly ImmutableDictionary<long, ModelFieldProperties> fields;
[Required]
public ImmutableDictionary<long, ModelFieldProperties> Fields
{
get { return fields; }
}
[Required]
public ModelSchemaProperties Properties
{
get { return properties; }
}
public SchemaDto(ModelSchemaProperties properties, ImmutableDictionary<long, ModelFieldProperties> fields)
{
Guard.NotNull(fields, nameof(fields));
Guard.NotNull(properties, nameof(properties));
this.properties = properties;
this.fields = fields;
}
public static SchemaDto Create(ModelSchema schema)
{
Guard.NotNull(schema, nameof(schema));
var fields = schema.Fields.ToDictionary(t => t.Key, t => t.Value.RawProperties).ToImmutableDictionary();
return new SchemaDto(schema.Properties, fields);
}
public ModelSchema ToModelSchema(ModelFieldFactory factory)
{
Guard.NotNull(factory, nameof(factory));
var schema = ModelSchema.Create(properties);
foreach (var field in fields)
{
schema = schema.AddField(field.Key, field.Value, factory);
}
return schema;
}
}
}

20
src/pinkparrot_core/PinkParrot.Core/Schema/ModelField.cs

@ -80,41 +80,21 @@ namespace PinkParrot.Core.Schema
public ModelField Hide()
{
if (isHidden)
{
throw new DomainException($"The field '{Name} is already hidden.");
}
return Update<ModelField>(clone => clone.isHidden = true);
}
public ModelField Show()
{
if (!isHidden)
{
throw new DomainException($"The field '{Name} is already visible.");
}
return Update<ModelField>(clone => clone.isHidden = false);
}
public ModelField Disable()
{
if (isDisabled)
{
throw new DomainException($"The field '{Name} is already disabled.");
}
return Update<ModelField>(clone => clone.isDisabled = true);
}
public ModelField Enable()
{
if (!isDisabled)
{
throw new DomainException($"The field '{Name} is already enabled.");
}
return Update<ModelField>(clone => clone.isDisabled = false);
}

9
src/pinkparrot_core/PinkParrot.Core/Schema/ModelFieldFactory.cs

@ -14,14 +14,15 @@ namespace PinkParrot.Core.Schema
{
public class ModelFieldFactory
{
private readonly Dictionary<Type, Func<long, ModelField>> factories = new Dictionary<Type, Func<long, ModelField>>();
private readonly Dictionary<Type, Func<long, ModelFieldProperties, ModelField>> factories
= new Dictionary<Type, Func<long, ModelFieldProperties, ModelField>>();
public ModelFieldFactory()
{
AddFactory<NumberFieldProperties>(id => new NumberField(id));
AddFactory<NumberFieldProperties>((id, p) => new NumberField(id, (NumberFieldProperties)p));
}
public ModelFieldFactory AddFactory<T>(Func<long, ModelField> factory) where T : ModelFieldProperties
public ModelFieldFactory AddFactory<T>(Func<long, ModelFieldProperties, ModelField> factory) where T : ModelFieldProperties
{
Guard.NotNull(factory, nameof(factory));
@ -41,7 +42,7 @@ namespace PinkParrot.Core.Schema
throw new InvalidOperationException("Field type is not supported.");
}
return factory(id);
return factory(id, properties);
}
}
}

5
src/pinkparrot_core/PinkParrot.Core/Schema/ModelField_Generic.cs

@ -46,9 +46,12 @@ namespace PinkParrot.Core.Schema
get { return properties; }
}
protected ModelField(long id)
protected ModelField(long id, T properties)
: base(id)
{
Guard.NotNull(properties, nameof(properties));
this.properties = properties;
}
public override ModelField Configure(ModelFieldProperties newProperties, IList<ValidationError> errors)

57
src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchema.cs

@ -18,7 +18,7 @@ namespace PinkParrot.Core.Schema
public sealed class ModelSchema
{
private readonly ModelSchemaProperties properties;
private readonly ImmutableDictionary<long, ModelField> fields;
private readonly ImmutableDictionary<long, ModelField> fieldsById;
private readonly Dictionary<string, ModelField> fieldsByName;
public ModelSchema(ModelSchemaProperties properties, ImmutableDictionary<long, ModelField> fields)
@ -26,30 +26,31 @@ namespace PinkParrot.Core.Schema
Guard.NotNull(fields, nameof(fields));
Guard.NotNull(properties, nameof(properties));
this.fields = fields;
this.properties = properties;
fieldsById = fields;
fieldsByName = fields.Values.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
}
public static ModelSchema Create(ModelSchemaProperties metadata)
public static ModelSchema Create(ModelSchemaProperties properties)
{
Guard.NotNull(properties, nameof(properties));
var errors = new List<ValidationError>();
metadata.Validate(errors);
properties.Validate(errors);
if (errors.Any())
{
throw new ValidationException("Failed to create a new model schema.", errors);
}
return new ModelSchema(metadata, ImmutableDictionary<long, ModelField>.Empty);
return new ModelSchema(properties, ImmutableDictionary<long, ModelField>.Empty);
}
public IReadOnlyDictionary<long, ModelField> Fields
public ImmutableDictionary<long, ModelField> Fields
{
get { return fields; }
get { return fieldsById; }
}
public ModelSchemaProperties Properties
@ -61,14 +62,14 @@ namespace PinkParrot.Core.Schema
{
Guard.NotNull(newMetadata, nameof(newMetadata));
return new ModelSchema(newMetadata, fields);
return new ModelSchema(newMetadata, fieldsById);
}
public ModelSchema AddField(long id, ModelFieldProperties fieldProperties, ModelFieldFactory factory)
{
var field = factory.CreateField(id, fieldProperties);
return SetField(field);
return ReplaceOrAddField(field);
}
public ModelSchema SetField(long fieldId, ModelFieldProperties fieldProperties)
@ -110,41 +111,35 @@ namespace PinkParrot.Core.Schema
return UpdateField(fieldId, field => field.Show());
}
public ModelSchema SetField(ModelField field)
public ModelSchema DeleteField(long fieldId)
{
Guard.NotNull(field, nameof(field));
if (fields.Values.Any(f => f.Name == field.Name && f.Id != field.Id))
{
throw new ValidationException($"A field with name '{field.Name}' already exists.");
}
return new ModelSchema(properties, fields.SetItem(field.Id, field));
return new ModelSchema(properties, fieldsById.Remove(fieldId));
}
public ModelSchema DeleteField(long fieldId)
private ModelSchema UpdateField(long fieldId, Func<ModelField, ModelField> updater)
{
if (!fields.ContainsKey(fieldId))
ModelField field;
if (!fieldsById.TryGetValue(fieldId, out field))
{
throw new ValidationException($"A field with id {fieldId} does not exist.");
throw new DomainObjectNotFoundException(fieldId.ToString(), typeof(ModelField));
}
return new ModelSchema(properties, fields.Remove(fieldId));
var newField = updater(field);
return ReplaceOrAddField(newField);
}
private ModelSchema UpdateField(long fieldId, Func<ModelField, ModelField> updater)
private ModelSchema ReplaceOrAddField(ModelField field)
{
ModelField field;
Guard.NotNull(field, nameof(field));
if (!fields.TryGetValue(fieldId, out field))
if (fieldsById.Values.Any(f => f.Name == field.Name && f.Id != field.Id))
{
throw new ValidationException($"Cannot update field with id '{fieldId}'.",
new ValidationError("Field does not exist.", "fieldId"));
throw new ValidationException($"A field with name '{field.Name}' already exists.");
}
var newField = updater(field);
return SetField(newField);
return new ModelSchema(properties, fieldsById.SetItem(field.Id, field));
}
public async Task ValidateAsync(PropertiesBag data)

4
src/pinkparrot_core/PinkParrot.Core/Schema/NumberField.cs

@ -27,8 +27,8 @@ namespace PinkParrot.Core.Schema
get { return Properties.MinValue; }
}
public NumberField(long id)
: base(id)
public NumberField(long id, NumberFieldProperties properties)
: base(id, properties)
{
}

27
src/pinkparrot_core/PinkParrot.Core/Schema/NumberFieldProperties.cs

@ -14,29 +14,52 @@ namespace PinkParrot.Core.Schema
[TypeName("Number")]
public sealed class NumberFieldProperties : ModelFieldProperties
{
public double? DefaultValue { get; }
public double? MaxValue { get; }
public double? MinValue { get; }
public string Placeholder { get; set; }
public NumberFieldProperties(
bool isRequired,
string name,
string label,
string hints,
string placeholder,
double? minValue,
double? maxValue)
double? maxValue,
double? defaultValue)
: base(isRequired, name, label, hints)
{
Placeholder = placeholder;
MinValue = minValue;
MaxValue = maxValue;
DefaultValue = defaultValue;
}
protected override void ValidateCore(IList<ValidationError> errors)
{
if (MaxValue.HasValue && MinValue.HasValue && MinValue.Value > MaxValue.Value)
if (MaxValue.HasValue && MinValue.HasValue)
{
errors.Add(new ValidationError("MinValue cannot be larger than max value", "MinValue", "MaxValue"));
}
if (DefaultValue.HasValue)
{
if (MinValue.HasValue && DefaultValue.Value < MinValue.Value)
{
errors.Add(new ValidationError("DefaultValue must be larger than the min value.", "DefaultValue"));
}
if (MaxValue.HasValue && DefaultValue.Value > MaxValue.Value)
{
errors.Add(new ValidationError("DefaultValue must be smaller than the max value.", "DefaultValue"));
}
}
}
}
}

2
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs

@ -13,7 +13,7 @@ namespace PinkParrot.Infrastructure.CQRS.Commands
{
public interface IDomainObjectRepository
{
Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, int version = 0) where TDomainObject : class, IAggregate;
Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate;
Task SaveAsync(IAggregate domainObject, Guid commitId);
}

5
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Commands/InMemoryCommandBus.cs

@ -54,6 +54,11 @@ namespace PinkParrot.Infrastructure.CQRS.Commands
context.MarkFailed(e);
}
}
if (context.Exception != null)
{
throw context.Exception;
}
}
}
}

15
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/DomainObject.cs

@ -52,20 +52,15 @@ namespace PinkParrot.Infrastructure.CQRS
{
ApplyEvent(envelopeToAdd);
}
else
{
version++;
}
}
protected void RaiseEvent(IEvent @event, bool disableApply = false)
{
Guard.NotNull(@event, nameof(@event));
var envelopeToAdd = EnvelopeFactory.ForEvent(@event, this);
uncomittedEvents.Add(envelopeToAdd);
if (!disableApply)
{
ApplyEvent(envelopeToAdd);
}
RaiseEvent(EnvelopeFactory.ForEvent(@event, this), disableApply);
}
void IAggregate.ApplyEvent(Envelope<IEvent> @event)

9
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/DefaultNameResolver.cs

@ -15,19 +15,16 @@ namespace PinkParrot.Infrastructure.CQRS.EventStore
{
private readonly string prefix;
public DefaultNameResolver()
: this(string.Empty)
{
}
public DefaultNameResolver(string prefix)
{
Guard.NotNullOrEmpty(prefix, nameof(prefix));
this.prefix = prefix;
}
public string GetStreamName(Type aggregateType, Guid id)
{
return string.Format(CultureInfo.InvariantCulture, "{0}{1}-{2}", prefix, char.ToLower(aggregateType.Name[0]) + aggregateType.Name.Substring(1), id);
return string.Format(CultureInfo.InvariantCulture, "{0}-{1}-{2}", prefix, char.ToLower(aggregateType.Name[0]) + aggregateType.Name.Substring(1), id);
}
}
}

101
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreBus.cs

@ -8,8 +8,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.SystemData;
using Microsoft.Extensions.Logging;
@ -26,7 +26,8 @@ namespace PinkParrot.Infrastructure.CQRS.EventStore
private readonly IEnumerable<ICatchEventConsumer> catchConsumers;
private readonly ILogger<EventStoreBus> logger;
private readonly IStreamPositionStorage positions;
private EventStoreAllCatchUpSubscription catchSubscription;
private EventStoreCatchUpSubscription catchSubscription;
private bool isLive;
public EventStoreBus(
ILogger<EventStoreBus> logger,
@ -52,15 +53,18 @@ namespace PinkParrot.Infrastructure.CQRS.EventStore
this.credentials = credentials;
this.liveConsumers = liveConsumers;
this.catchConsumers = catchConsumers;
Subscribe();
}
private void Subscribe()
public void Subscribe(string streamName = "$all")
{
var position = positions.ReadPosition();
Guard.NotNullOrEmpty(streamName, nameof(streamName));
if (catchSubscription != null)
{
return;
}
var now = DateTime.UtcNow;
var position = positions.ReadPosition();
logger.LogInformation($"Subscribing from: {0}", position);
@ -70,58 +74,63 @@ namespace PinkParrot.Infrastructure.CQRS.EventStore
true,
true);
catchSubscription = connection.SubscribeToAllFrom(position, settings, (s, resolvedEvent) =>
catchSubscription =
connection.SubscribeToStreamFrom(streamName, position, settings,
OnEvent,
OnLiveProcessingStarted,
userCredentials: credentials);
}
private void OnEvent(EventStoreCatchUpSubscription subscription, ResolvedEvent resolvedEvent)
{
try
{
var requireUpdate = false;
if (resolvedEvent.OriginalEvent.EventStreamId.StartsWith("$", StringComparison.OrdinalIgnoreCase))
{
return;
}
Debug.WriteLine($"Last Position: {catchSubscription.LastProcessedPosition}");
try
if (!liveConsumers.Any() && !catchConsumers.Any())
{
if (resolvedEvent.OriginalEvent.EventStreamId.StartsWith("$", StringComparison.OrdinalIgnoreCase))
{
return;
}
return;
}
if (liveConsumers.Any() || catchConsumers.Any())
{
requireUpdate = true;
var @event = formatter.Parse(resolvedEvent);
var @event = formatter.Parse(resolvedEvent);
if (isLive)
{
DispatchConsumers(liveConsumers, @event).Wait();
}
if (resolvedEvent.Event.Created > now)
{
Dispatch(liveConsumers, @event);
}
DispatchConsumers(catchConsumers, @event).Wait();
}
finally
{
positions.WritePosition(resolvedEvent.OriginalEventNumber);
}
}
Dispatch(catchConsumers, @event);
}
private void OnLiveProcessingStarted(EventStoreCatchUpSubscription subscription)
{
isLive = true;
}
requireUpdate = requireUpdate || catchSubscription.LastProcessedPosition.CommitPosition % 2 == 0;
}
finally
{
if (requireUpdate)
{
positions.WritePosition(catchSubscription.LastProcessedPosition);
}
}
}, userCredentials: credentials);
private Task DispatchConsumers(IEnumerable<IEventConsumer> consumers, Envelope<IEvent> @event)
{
return Task.WhenAll(consumers.Select(c => DispatchConsumer(@event, c)).ToList());
}
private void Dispatch(IEnumerable<IEventConsumer> consumers, Envelope<IEvent> @event)
private async Task DispatchConsumer(Envelope<IEvent> @event, IEventConsumer consumer)
{
foreach (var consumer in consumers)
try
{
try
{
consumer.On(@event);
}
catch (Exception ex)
{
var eventId = new EventId(10001, "EventConsumeFailed");
await consumer.On(@event);
}
catch (Exception ex)
{
var eventId = new EventId(10001, "EventConsumeFailed");
logger.LogError(eventId, ex, "'{0}' failed to handle event {1} ({2})", consumer, @event.Payload, @event.Headers.EventId());
}
logger.LogError(eventId, ex, "'{0}' failed to handle event {1} ({2})", consumer, @event.Payload, @event.Headers.EventId());
}
}
}

2
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs

@ -51,7 +51,7 @@ namespace PinkParrot.Infrastructure.CQRS.EventStore
this.nameResolver = nameResolver;
}
public async Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, int version = 0) where TDomainObject : class, IAggregate
public async Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate
{
Guard.GreaterThan(version, 0, nameof(version));

6
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs

@ -6,14 +6,12 @@
// All rights reserved.
// ==========================================================================
using EventStore.ClientAPI;
namespace PinkParrot.Infrastructure.CQRS.EventStore
{
public interface IStreamPositionStorage
{
Position? ReadPosition();
int? ReadPosition();
void WritePosition(Position position);
void WritePosition(int position);
}
}

5
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Events/IEventConsumer.cs

@ -5,10 +5,13 @@
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
namespace PinkParrot.Infrastructure.CQRS.Events
{
public interface IEventConsumer
{
void On(Envelope<IEvent> @event);
Task On(Envelope<IEvent> @event);
}
}

68
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/ReflectionExtensions.cs

@ -0,0 +1,68 @@
// ==========================================================================
// ReflectionExtensions.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace PinkParrot.Infrastructure.Reflection
{
public static class ReflectionExtensions
{
public static PropertyInfo[] GetPublicProperties(this Type type)
{
const BindingFlags bindingFlags =
BindingFlags.FlattenHierarchy |
BindingFlags.Public |
BindingFlags.Instance;
if (!type.GetTypeInfo().IsInterface)
{
return type.GetProperties(bindingFlags);
}
var flattenProperties = new HashSet<PropertyInfo>();
var considered = new List<Type>
{
type
};
var queue = new Queue<Type>();
queue.Enqueue(type);
while (queue.Count > 0)
{
var subType = queue.Dequeue();
foreach (var subInterface in subType.GetInterfaces())
{
if (considered.Contains(subInterface))
{
continue;
}
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
var typeProperties = subType.GetProperties(bindingFlags);
foreach (var property in typeProperties)
{
flattenProperties.Add(property);
}
}
return flattenProperties.ToArray();
}
}
}

4
src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/SimpleMapper.cs

@ -111,11 +111,11 @@ namespace PinkParrot.Infrastructure.Reflection
var dstType = typeof(TDestination);
var srcType = typeof(TSource);
var destinationProperties = dstType.GetProperties();
var destinationProperties = dstType.GetPublicProperties();
var newMappers = new List<PropertyMapper>();
foreach (var srcProperty in srcType.GetProperties().Where(x => x.CanRead))
foreach (var srcProperty in srcType.GetPublicProperties().Where(x => x.CanRead))
{
var dstProperty = destinationProperties.FirstOrDefault(x => x.Name == srcProperty.Name);

14
src/pinkparrot_read/PinkParrot.Read/IEntity.cs

@ -1,14 +0,0 @@
using System;
namespace PinkParrot.Read.Models
{
public interface IModelSchemaRM1
{
DateTime Created { get; set; }
string Hints { get; set; }
string Label { get; set; }
DateTime Modified { get; set; }
string Name { get; set; }
Guid SchemaId { get; set; }
}
}

11
src/pinkparrot_read/PinkParrot.Read/IModelSchemaRM.cs

@ -1,11 +0,0 @@
using System;
namespace PinkParrot.Read.Models
{
public interface IModelSchemaRM
{
DateTime Created { get; set; }
DateTime Modified { get; set; }
Guid SchemaId { get; set; }
}
}

26
src/pinkparrot_read/PinkParrot.Read/Repositories/EntityWithSchema.cs

@ -0,0 +1,26 @@
// ==========================================================================
// EntityWithSchema.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using PinkParrot.Core.Schema;
namespace PinkParrot.Read.Repositories
{
public sealed class EntityWithSchema
{
public IModelSchemaEntity Entity { get; }
public ModelSchema Schema { get; }
internal EntityWithSchema(IModelSchemaEntity entity, ModelSchema schema)
{
Entity = entity;
Schema = schema;
}
}
}

2
src/pinkparrot_read/PinkParrot.Read/Models/IEntity.cs → src/pinkparrot_read/PinkParrot.Read/Repositories/IEntity.cs

@ -8,7 +8,7 @@
using System;
namespace PinkParrot.Read.Models
namespace PinkParrot.Read.Repositories
{
public interface IEntity
{

14
src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaEntity.cs

@ -0,0 +1,14 @@
// ==========================================================================
// IModelSchemaEntity.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
namespace PinkParrot.Read.Repositories
{
public interface IModelSchemaEntity : ITenantEntity
{
string Name { get; }
}
}

9
src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaRepository.cs

@ -9,12 +9,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using PinkParrot.Read.Models;
namespace PinkParrot.Read.Repositories
{
public interface IModelSchemaRepository
{
Task<List<ModelSchemaListRM>> QueryAllAsync(Guid tenantId);
Task<List<IModelSchemaEntity>> QueryAllAsync(Guid tenantId);
Task<Guid?> FindSchemaIdAsync(Guid tenantId, string name);
Task<EntityWithSchema> FindSchemaAsync(Guid tenantId, string name);
Task<EntityWithSchema> FindSchemaAsync(Guid schemaId);
}
}

4
src/pinkparrot_read/PinkParrot.Read/Models/ITenantEntity.cs → src/pinkparrot_read/PinkParrot.Read/Repositories/ITenantEntity.cs

@ -8,9 +8,9 @@
using System;
namespace PinkParrot.Read.Models
namespace PinkParrot.Read.Repositories
{
public interface ITenantEntity
public interface ITenantEntity : IEntity
{
Guid TenantId { get; set; }
}

19
src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/EntityMapper.cs

@ -8,10 +8,11 @@
using System;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json;
using PinkParrot.Infrastructure.CQRS;
using PinkParrot.Infrastructure.MongoDb;
using PinkParrot.Read.Models;
namespace PinkParrot.Read.Repositories.Implementations
{
@ -33,6 +34,20 @@ namespace PinkParrot.Read.Repositories.Implementations
return Update(entity, headers);
}
public static BsonDocument ToJsonBsonDocument<T>(this T value, JsonSerializerSettings settings)
{
var json = JsonConvert.SerializeObject(value, settings).Replace("$type", "§type");
return BsonDocument.Parse(json);
}
public static T ToJsonObject<T>(this BsonDocument document, JsonSerializerSettings settings)
{
var json = document.ToJson().Replace("§type", "$type");
return JsonConvert.DeserializeObject<T>(json, settings);
}
public static T Update<T>(T entity, EnvelopeHeaders headers) where T : IEntity
{
var timestamp = headers.Timestamp().ToDateTimeUtc();
@ -64,7 +79,7 @@ namespace PinkParrot.Read.Repositories.Implementations
updater(entity);
await collection.ReplaceOneAsync(t => t.Id == entity.Id, entity);
var result = await collection.ReplaceOneAsync(t => t.Id == entity.Id, entity);
}
}
}

11
src/pinkparrot_read/PinkParrot.Read/Models/ModelSchemaListRM.cs → src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaEntity.cs

@ -1,5 +1,5 @@
// ==========================================================================
// ModelSchemaRM.cs
// MongoModelSchemaEntity.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
@ -9,11 +9,10 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using PinkParrot.Infrastructure;
namespace PinkParrot.Read.Models
namespace PinkParrot.Read.Repositories.Implementations
{
public sealed class ModelSchemaListRM : IEntity, ITenantEntity
public sealed class MongoModelSchemaEntity : IModelSchemaEntity
{
[BsonId]
[BsonElement]
@ -39,5 +38,9 @@ namespace PinkParrot.Read.Models
[BsonRequired]
[BsonElement]
public bool IsDeleted { get; set; }
[BsonRequired]
[BsonElement]
public BsonDocument Schema { get; set; }
}
}

66
src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaListRepository.cs

@ -1,66 +0,0 @@
// ==========================================================================
// MongoModelSchemaRepository.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Driver;
using PinkParrot.Events.Schema;
using PinkParrot.Infrastructure.CQRS;
using PinkParrot.Infrastructure.CQRS.Events;
using PinkParrot.Infrastructure.Dispatching;
using PinkParrot.Infrastructure.MongoDb;
using PinkParrot.Infrastructure.Tasks;
using PinkParrot.Read.Models;
namespace PinkParrot.Read.Repositories.Implementations
{
public sealed class MongoModelSchemaListRepository : MongoRepositoryBase<ModelSchemaListRM>, IModelSchemaRepository, ICatchEventConsumer
{
public MongoModelSchemaListRepository(IMongoDatabase database)
: base(database)
{
}
protected override Task SetupCollectionAsync(IMongoCollection<ModelSchemaListRM> collection)
{
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Id));
}
public IQueryable<ModelSchemaListRM> QuerySchemas()
{
return Collection.AsQueryable();
}
public Task<List<ModelSchemaListRM>> QueryAllAsync(Guid tenantId)
{
return Collection.Find(s => s.TenantId == tenantId && s.IsDeleted == false).ToListAsync();
}
public void On(ModelSchemaUpdated @event, EnvelopeHeaders headers)
{
Collection.UpdateAsync(headers, e => e.Name = @event.Properties.Name).Forget();
}
public void On(ModelSchemaDeleted @event, EnvelopeHeaders headers)
{
Collection.UpdateAsync(headers, e => e.IsDeleted = true).Forget();
}
public void On(ModelSchemaCreated @event, EnvelopeHeaders headers)
{
Collection.CreateAsync(headers, e => e.Name = @event.Properties.Name);
}
public void On(Envelope<IEvent> @event)
{
this.DispatchAction(@event.Payload, @event.Headers);
}
}
}

172
src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaRepository.cs

@ -0,0 +1,172 @@
// ==========================================================================
// MongoModelSchemaRepository.cs
// PinkParrot Headless CMS
// ==========================================================================
// Copyright (c) PinkParrot Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json;
using PinkParrot.Core.Schema;
using PinkParrot.Core.Schema.Json;
using PinkParrot.Events.Schema;
using PinkParrot.Infrastructure;
using PinkParrot.Infrastructure.CQRS;
using PinkParrot.Infrastructure.CQRS.Events;
using PinkParrot.Infrastructure.Dispatching;
using PinkParrot.Infrastructure.MongoDb;
using PinkParrot.Infrastructure.Tasks;
namespace PinkParrot.Read.Repositories.Implementations
{
public sealed class MongoModelSchemaRepository : MongoRepositoryBase<MongoModelSchemaEntity>, IModelSchemaRepository, ICatchEventConsumer
{
private readonly JsonSerializerSettings serializerSettings;
private readonly ModelFieldFactory fieldFactory;
public MongoModelSchemaRepository(IMongoDatabase database, JsonSerializerSettings serializerSettings, ModelFieldFactory fieldFactory)
: base(database)
{
Guard.NotNull(serializerSettings, nameof(serializerSettings));
Guard.NotNull(fieldFactory, nameof(fieldFactory));
this.serializerSettings = serializerSettings;
this.fieldFactory = fieldFactory;
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoModelSchemaEntity> collection)
{
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Name));
}
public async Task<List<IModelSchemaEntity>> QueryAllAsync(Guid tenantId)
{
var entities = await Collection.Find(s => s.TenantId == tenantId && !s.IsDeleted).ToListAsync();
return entities.OfType<IModelSchemaEntity>().ToList();
}
public async Task<EntityWithSchema> FindSchemaAsync(Guid tenantId, string name)
{
var entity =
await Collection.Find(s => s.Name == name && s.TenantId == tenantId && !s.IsDeleted)
.FirstOrDefaultAsync();
return entity != null ? new EntityWithSchema(entity, Deserialize(entity)) : null;
}
public async Task<EntityWithSchema> FindSchemaAsync(Guid schemaId)
{
var entity =
await Collection.Find(s => s.Id == schemaId && !s.IsDeleted)
.FirstOrDefaultAsync();
return entity != null ? new EntityWithSchema(entity, Deserialize(entity)) : null;
}
public async Task<Guid?> FindSchemaIdAsync(Guid tenantId, string name)
{
var entity =
await Collection.Find(s => s.Name == name & s.TenantId == tenantId && !s.IsDeleted)
.Project<MongoModelSchemaEntity>(Projection.Include(x => x.Id)).FirstOrDefaultAsync();
return entity?.Id;
}
public Task On(ModelSchemaDeleted @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, e => e.IsDeleted = true);
}
public Task On(ModelFieldAdded @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => s.AddField(@event.FieldId, @event.Properties, fieldFactory));
}
public Task On(ModelFieldDeleted @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => s.DeleteField(@event.FieldId));
}
public Task On(ModelFieldDisabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => s.DisableField(@event.FieldId));
}
public Task On(ModelFieldEnabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => s.EnableField(@event.FieldId));
}
public Task On(ModelFieldHidden @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => s.HideField(@event.FieldId));
}
public Task On(ModelFieldShown @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => s.ShowField(@event.FieldId));
}
public Task On(ModelFieldUpdated @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => s.SetField(@event.FieldId, @event.Properties));
}
public Task On(ModelSchemaUpdated @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, e =>
{
e.Name = @event.Properties.Name;
UpdateSchema(e, s => s.Update(@event.Properties));
});
}
public Task On(ModelSchemaCreated @event, EnvelopeHeaders headers)
{
return Collection.CreateAsync(headers, e =>
{
e.Name = @event.Properties.Name;
Serialize(e, ModelSchema.Create(@event.Properties));
});
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
private void UpdateSchema(MongoModelSchemaEntity entity, Func<ModelSchema, ModelSchema> updater)
{
var currentSchema = Deserialize(entity);
currentSchema = updater(currentSchema);
Serialize(entity, currentSchema);
}
private Task UpdateSchema(EnvelopeHeaders headers, Func<ModelSchema, ModelSchema> updater)
{
return Collection.UpdateAsync(headers, e=> UpdateSchema(e, updater));
}
private void Serialize(MongoModelSchemaEntity entity, ModelSchema schema)
{
entity.Schema = SchemaDto.Create(schema).ToJsonBsonDocument(serializerSettings);
}
private ModelSchema Deserialize(MongoModelSchemaEntity entity)
{
return entity?.Schema.ToJsonObject<SchemaDto>(serializerSettings).ToModelSchema(fieldFactory);
}
}
}

2
src/pinkparrot_read/PinkParrot.Read/Services/IModelSchemaProvider.cs

@ -13,6 +13,6 @@ namespace PinkParrot.Read.Services
{
public interface IModelSchemaProvider
{
Task<Guid> FindSchemaIdByNameAsync(string name);
Task<Guid?> FindSchemaIdByNameAsync(Guid tenantId, string name);
}
}

2
src/pinkparrot_read/PinkParrot.Read/Services/ITenantProvider.cs

@ -13,6 +13,6 @@ namespace PinkParrot.Read.Services
{
public interface ITenantProvider
{
Task<Guid> ProvideTenantIdByDomainAsync(string domain);
Task<Guid?> ProvideTenantIdByDomainAsync(string domain);
}
}

69
src/pinkparrot_read/PinkParrot.Read/Services/Implementations/ModelSchemaProvider.cs

@ -8,14 +8,77 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using PinkParrot.Events.Schema;
using PinkParrot.Infrastructure;
using PinkParrot.Infrastructure.CQRS;
using PinkParrot.Infrastructure.CQRS.Events;
using PinkParrot.Read.Repositories;
// ReSharper disable InvertIf
namespace PinkParrot.Read.Services.Implementations
{
public class ModelSchemaProvider : IModelSchemaProvider
public class ModelSchemaProvider : IModelSchemaProvider, ILiveEventConsumer
{
public Task<Guid> FindSchemaIdByNameAsync(string name)
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IMemoryCache cache;
private readonly IModelSchemaRepository repository;
public ModelSchemaProvider(IMemoryCache cache, IModelSchemaRepository repository)
{
Guard.NotNull(cache, nameof(cache));
Guard.NotNull(repository, nameof(repository));
this.cache = cache;
this.repository = repository;
}
public async Task<Guid?> FindSchemaIdByNameAsync(Guid tenantId, string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
var cacheKey = BuildModelsCacheKey(tenantId, name);
var cacheItem = cache.Get<EntityWithSchema>(cacheKey);
if (cacheItem == null)
{
cacheItem = await repository.FindSchemaAsync(tenantId, name) ?? new EntityWithSchema(null, null);
cache.Set(cacheKey, cacheItem, CacheDuration);
if (cacheItem.Entity != null)
{
cache.Set(BuildNamesCacheKey(cacheItem.Entity.Id), cacheItem.Entity.Name, CacheDuration);
}
}
return cacheItem.Entity?.Id;
}
public Task On(Envelope<IEvent> @event)
{
if (@event.Payload is ModelSchemaUpdated || @event.Payload is ModelSchemaDeleted)
{
var oldName = cache.Get<string>(BuildNamesCacheKey(@event.Headers.AggregateId()));
if (oldName != null)
{
cache.Remove(BuildModelsCacheKey(@event.Headers.TenantId(), oldName));
}
}
return Task.FromResult(true);
}
private static string BuildModelsCacheKey(Guid tenantId, string name)
{
return $"Schemas_Models_{tenantId}_{name}";
}
private static string BuildNamesCacheKey(Guid schemaId)
{
return Task.FromResult(Guid.Empty);
return $"Schema_Names_{schemaId}";
}
}
}

5
src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoPositions.cs

@ -19,9 +19,6 @@ namespace PinkParrot.Read.Services.Implementations
public ObjectId Id { get; set; }
[BsonElement]
public long CommitPosition { get; set; }
[BsonElement]
public long PreparePosition { get; set; }
public int? Position { get; set; }
}
}

31
src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoStreamPositionsStorage.cs

@ -6,13 +6,12 @@
// All rights reserved.
// ==========================================================================
using EventStore.ClientAPI;
using MongoDB.Bson;
using MongoDB.Driver;
using PinkParrot.Infrastructure.CQRS.EventStore;
using PinkParrot.Infrastructure.MongoDb;
//// ReSharper disable once ConvertIfStatementToNullCoalescingExpression
// ReSharper disable InvertIf
namespace PinkParrot.Read.Services.Implementations
{
@ -25,35 +24,23 @@ namespace PinkParrot.Read.Services.Implementations
{
}
public Position? ReadPosition()
public int? ReadPosition()
{
var document = Collection.Find(t => t.Id == Id).FirstOrDefault<MongoPosition, MongoPosition>();
return document != null ? new Position(document.CommitPosition, document.PreparePosition) : Position.Start;
}
public void WritePosition(Position position)
{
var document = Collection.Find(t => t.Id == Id).FirstOrDefault<MongoPosition, MongoPosition>();
var isFound = document != null;
if (document == null)
{
document = new MongoPosition { Id = Id };
}
document.CommitPosition = position.CommitPosition;
document.PreparePosition = position.PreparePosition;
if (isFound)
{
Collection.ReplaceOne(t => t.Id == Id, document);
}
else
{
Collection.InsertOne(document);
}
return document.Position;
}
public void WritePosition(int position)
{
Collection.UpdateOne(t => t.Id == Id, Update.Set(t => t.Position, position));
}
}
}

4
src/pinkparrot_read/PinkParrot.Read/Services/Implementations/TenantProvider.cs

@ -13,9 +13,9 @@ namespace PinkParrot.Read.Services.Implementations
{
public sealed class TenantProvider : ITenantProvider
{
public Task<Guid> ProvideTenantIdByDomainAsync(string domain)
public Task<Guid?> ProvideTenantIdByDomainAsync(string domain)
{
return Task.FromResult(Guid.Empty);
return Task.FromResult<Guid?>(Guid.Empty);
}
}
}

1
src/pinkparrot_read/PinkParrot.Read/project.json

@ -2,6 +2,7 @@
"version": "1.0.0-*",
"dependencies": {
"Microsoft.Extensions.Caching.Memory": "1.0.0",
"MongoDB.Driver": "2.3.0-rc1",
"NETStandard.Library": "1.6.0",
"NodaTime": "2.0.0-alpha20160729",

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/AddModelField.cs

@ -11,7 +11,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class AddModelField : AggregateCommand
public class AddModelField : TenantCommand
{
public ModelFieldProperties Properties { get; set; }
}

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/DeleteModelField.cs

@ -10,7 +10,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class DeleteModelField : AggregateCommand
public class DeleteModelField : TenantCommand
{
public long FieldId { get; set; }
}

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/DeleteModelSchema.cs

@ -10,7 +10,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class DeleteModelSchema : AggregateCommand
public class DeleteModelSchema : TenantCommand
{
}
}

4
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/DisableModelField.cs

@ -6,11 +6,9 @@
// All rights reserved.
// ==========================================================================
using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class DisableModelField : AggregateCommand
public class DisableModelField : TenantCommand
{
public long FieldId { get; set; }
}

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/EnableModelField.cs

@ -10,7 +10,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class EnableModelField : AggregateCommand
public class EnableModelField : TenantCommand
{
public long FieldId { get; set; }
}

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/HideModelField.cs

@ -10,7 +10,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class HideModelField : AggregateCommand
public class HideModelField : TenantCommand
{
public long FieldId { get; set; }
}

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/ShowModelField.cs

@ -10,7 +10,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class ShowModelField : AggregateCommand
public class ShowModelField : TenantCommand
{
public long FieldId { get; set; }
}

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/UpdateModelField.cs

@ -11,7 +11,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class UpdateModelField : AggregateCommand
public class UpdateModelField : TenantCommand
{
public long FieldId { get; set; }

2
src/pinkparrot_write/PinkParrot.Write/Schema/Commands/UpdateModelSchema.cs

@ -11,7 +11,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write.Schema.Commands
{
public class UpdateModelSchema : AggregateCommand
public class UpdateModelSchema : TenantCommand
{
public ModelSchemaProperties Properties { get; set; }
}

2
src/pinkparrot_write/PinkParrot.Write/Schema/ModelSchemaCommandHandler.cs

@ -18,7 +18,7 @@ namespace PinkParrot.Write.Schema
{
public Task<bool> HandleAsync(CommandContext context)
{
return this.DispatchActionAsync(context.Command, context);
return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context);
}
public Task On(AddModelField command, CommandContext context)

25
src/pinkparrot_write/PinkParrot.Write/Schema/ModelSchemaDomainObject.cs

@ -9,6 +9,7 @@
using System;
using PinkParrot.Core.Schema;
using PinkParrot.Events.Schema;
using PinkParrot.Infrastructure;
using PinkParrot.Infrastructure.CQRS;
using PinkParrot.Infrastructure.CQRS.Events;
using PinkParrot.Infrastructure.Dispatching;
@ -45,56 +46,56 @@ namespace PinkParrot.Write.Schema
this.fieldFactory = fieldFactory;
}
protected void Apply(ModelFieldAdded @event)
public void On(ModelFieldAdded @event)
{
schema = schema.AddField(@event.FieldId, @event.Properties, fieldFactory);
totalFields++;
}
protected void Apply(ModelSchemaCreated @event)
public void On(ModelSchemaCreated @event)
{
tenantId = @event.TenantId;
schema = ModelSchema.Create(@event.Properties);
}
protected void Apply(ModelFieldUpdated @event)
public void On(ModelFieldUpdated @event)
{
schema = schema.SetField(@event.FieldId, @event.Properties);
}
public void Apply(ModelFieldHidden @event)
public void On(ModelFieldHidden @event)
{
schema = schema.HideField(@event.FieldId);
}
public void Apply(ModelFieldShown @event)
public void On(ModelFieldShown @event)
{
schema = schema.ShowField(@event.FieldId);
}
public void Apply(ModelFieldDisabled @event)
public void On(ModelFieldDisabled @event)
{
schema = schema.DisableField(@event.FieldId);
}
public void Apply(ModelFieldEnabled @event)
public void On(ModelFieldEnabled @event)
{
schema = schema.EnableField(@event.FieldId);
}
protected void Apply(ModelSchemaUpdated @event)
public void On(ModelSchemaUpdated @event)
{
schema = schema.Update(@event.Properties);
}
protected void Apply(ModelFieldDeleted @event)
public void On(ModelFieldDeleted @event)
{
schema = schema.DeleteField(@event.FieldId);
}
protected void Apply(ModelSchemaDeleted @event)
public void On(ModelSchemaDeleted @event)
{
isDeleted = false;
}
@ -197,7 +198,7 @@ namespace PinkParrot.Write.Schema
{
if (schema != null)
{
throw new InvalidOperationException("Schema has already been created.");
throw new DomainException("Schema has already been created.");
}
}
@ -205,7 +206,7 @@ namespace PinkParrot.Write.Schema
{
if (isDeleted || schema == null)
{
throw new InvalidOperationException("Schema has already been deleted or not created yet.");
throw new DomainException("Schema has already been deleted or not created yet.");
}
}

2
src/pinkparrot_write/PinkParrot.Write/TenantCommand.cs

@ -11,7 +11,7 @@ using PinkParrot.Infrastructure.CQRS.Commands;
namespace PinkParrot.Write
{
public abstract class TenantCommand : AggregateCommand
public abstract class TenantCommand : AggregateCommand, ITenantCommand
{
public Guid TenantId { get; set; }
}

Loading…
Cancel
Save