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