mirror of https://github.com/Squidex/squidex.git
60 changed files with 820 additions and 360 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue