mirror of https://github.com/Squidex/squidex.git
47 changed files with 1302 additions and 575 deletions
@ -0,0 +1,104 @@ |
|||||
|
// ==========================================================================
|
||||
|
// CachedGraphQLInvoker.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Read.Apps; |
||||
|
using Squidex.Domain.Apps.Read.Assets.Repositories; |
||||
|
using Squidex.Domain.Apps.Read.Contents.Repositories; |
||||
|
using Squidex.Domain.Apps.Read.Schemas.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Domain.Apps.Read.Utils; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using System; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
|
||||
|
// ReSharper disable InvertIf
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
||||
|
{ |
||||
|
public sealed class CachingGraphQLInvoker : CachingProviderBase, IGraphQLInvoker, IEventConsumer |
||||
|
{ |
||||
|
private readonly IContentRepository contentRepository; |
||||
|
private readonly IAssetRepository assetRepository; |
||||
|
private readonly ISchemaRepository schemaRepository; |
||||
|
|
||||
|
public string Name |
||||
|
{ |
||||
|
get { return GetType().Name; } |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get { return "^(schema-)|(apps-)"; } |
||||
|
} |
||||
|
|
||||
|
public CachingGraphQLInvoker(IMemoryCache cache, ISchemaRepository schemaRepository, IAssetRepository assetRepository, IContentRepository contentRepository) |
||||
|
: base(cache) |
||||
|
{ |
||||
|
Guard.NotNull(schemaRepository, nameof(schemaRepository)); |
||||
|
Guard.NotNull(assetRepository, nameof(assetRepository)); |
||||
|
Guard.NotNull(contentRepository, nameof(contentRepository)); |
||||
|
|
||||
|
this.schemaRepository = schemaRepository; |
||||
|
this.assetRepository = assetRepository; |
||||
|
this.contentRepository = contentRepository; |
||||
|
} |
||||
|
|
||||
|
public Task ClearAsync() |
||||
|
{ |
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
public Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
if (@event.Payload is AppEvent appEvent) |
||||
|
{ |
||||
|
Cache.Remove(CreateCacheKey(appEvent.AppId.Id)); |
||||
|
} |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
public async Task<object> QueryAsync(IAppEntity appEntity, GraphQLQuery query) |
||||
|
{ |
||||
|
Guard.NotNull(appEntity, nameof(appEntity)); |
||||
|
Guard.NotNull(query, nameof(query)); |
||||
|
|
||||
|
var modelContext = await GetModelAsync(appEntity); |
||||
|
var queryContext = new QueryContext(appEntity, contentRepository, assetRepository); |
||||
|
|
||||
|
return await modelContext.ExecuteAsync(queryContext, query); |
||||
|
} |
||||
|
|
||||
|
private async Task<GraphQLModel> GetModelAsync(IAppEntity appEntity) |
||||
|
{ |
||||
|
var cacheKey = CreateCacheKey(appEntity.Id); |
||||
|
|
||||
|
var modelContext = Cache.Get<GraphQLModel>(cacheKey); |
||||
|
|
||||
|
if (modelContext == null) |
||||
|
{ |
||||
|
var schemas = await schemaRepository.QueryAllAsync(appEntity.Id); |
||||
|
|
||||
|
modelContext = new GraphQLModel(appEntity, schemas.Where(x => x.IsPublished)); |
||||
|
|
||||
|
Cache.Set(cacheKey, modelContext); |
||||
|
} |
||||
|
|
||||
|
return modelContext; |
||||
|
} |
||||
|
|
||||
|
private static object CreateCacheKey(Guid appId) |
||||
|
{ |
||||
|
return $"GraphQLModel_{appId}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,187 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphQLContext.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using GraphQL; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Read.Apps; |
||||
|
using Squidex.Domain.Apps.Read.Contents.GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Read.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using GraphQLSchema = GraphQL.Types.Schema; |
||||
|
|
||||
|
// ReSharper disable InvertIf
|
||||
|
// ReSharper disable ParameterHidesMember
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
||||
|
{ |
||||
|
public sealed class GraphQLModel : IGraphQLContext |
||||
|
{ |
||||
|
private readonly Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> fieldInfos; |
||||
|
private readonly Dictionary<Guid, IGraphType> schemaTypes = new Dictionary<Guid, IGraphType>(); |
||||
|
private readonly Dictionary<Guid, ISchemaEntity> schemas; |
||||
|
private readonly PartitionResolver partitionResolver; |
||||
|
private readonly IGraphType assetType = new AssetGraphType(); |
||||
|
private readonly GraphQLSchema graphQLSchema; |
||||
|
|
||||
|
public GraphQLModel(IAppEntity appEntity, IEnumerable<ISchemaEntity> schemas) |
||||
|
{ |
||||
|
partitionResolver = appEntity.PartitionResolver; |
||||
|
|
||||
|
var defaultResolver = |
||||
|
new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName)); |
||||
|
|
||||
|
IGraphType assetListType = new ListGraphType(new NonNullGraphType(assetType)); |
||||
|
|
||||
|
var stringInfos = |
||||
|
(new StringGraphType(), defaultResolver); |
||||
|
|
||||
|
var booleanInfos = |
||||
|
(new BooleanGraphType(), defaultResolver); |
||||
|
|
||||
|
var numberInfos = |
||||
|
(new FloatGraphType(), defaultResolver); |
||||
|
|
||||
|
var dateTimeInfos = |
||||
|
(new DateGraphType(), defaultResolver); |
||||
|
|
||||
|
var jsonInfos = |
||||
|
(new ObjectGraphType(), defaultResolver); |
||||
|
|
||||
|
var geolocationInfos = |
||||
|
(new ObjectGraphType(), defaultResolver); |
||||
|
|
||||
|
fieldInfos = new Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> |
||||
|
{ |
||||
|
{ |
||||
|
typeof(StringField), |
||||
|
field => stringInfos |
||||
|
}, |
||||
|
{ |
||||
|
typeof(BooleanField), |
||||
|
field => booleanInfos |
||||
|
}, |
||||
|
{ |
||||
|
typeof(NumberField), |
||||
|
field => numberInfos |
||||
|
}, |
||||
|
{ |
||||
|
typeof(DateTimeField), |
||||
|
field => dateTimeInfos |
||||
|
}, |
||||
|
{ |
||||
|
typeof(JsonField), |
||||
|
field => jsonInfos |
||||
|
}, |
||||
|
{ |
||||
|
typeof(GeolocationField), |
||||
|
field => geolocationInfos |
||||
|
}, |
||||
|
{ |
||||
|
typeof(AssetsField), |
||||
|
field => |
||||
|
{ |
||||
|
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
||||
|
{ |
||||
|
var context = (QueryContext)c.UserContext; |
||||
|
var contentIds = c.Source.GetOrDefault(c.FieldName); |
||||
|
|
||||
|
return context.GetReferencedAssets(contentIds); |
||||
|
}); |
||||
|
|
||||
|
return (assetListType, resolver); |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
typeof(ReferencesField), |
||||
|
field => |
||||
|
{ |
||||
|
var schemaId = ((ReferencesField)field).Properties.SchemaId; |
||||
|
var schemaType = GetSchemaType(schemaId); |
||||
|
|
||||
|
if (schemaType == null) |
||||
|
{ |
||||
|
return (null, null); |
||||
|
} |
||||
|
|
||||
|
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
||||
|
{ |
||||
|
var context = (QueryContext)c.UserContext; |
||||
|
var contentIds = c.Source.GetOrDefault(c.FieldName); |
||||
|
|
||||
|
return context.GetReferencedContents(schemaId, contentIds); |
||||
|
}); |
||||
|
|
||||
|
var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); |
||||
|
|
||||
|
return (schemaFieldType, resolver); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
this.schemas = schemas.ToDictionary(x => x.Id); |
||||
|
|
||||
|
graphQLSchema = new GraphQLSchema { Query = new ContentQueryType(this, this.schemas.Values) }; |
||||
|
} |
||||
|
|
||||
|
public async Task<object> ExecuteAsync(QueryContext context, GraphQLQuery query) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
|
||||
|
var result = await new DocumentExecuter().ExecuteAsync(options => |
||||
|
{ |
||||
|
options.Query = query.Query; |
||||
|
options.Schema = graphQLSchema; |
||||
|
options.Inputs = query.Variables?.ToInputs() ?? new Inputs(); |
||||
|
options.UserContext = context; |
||||
|
options.OperationName = query.OperationName; |
||||
|
}).ConfigureAwait(false); |
||||
|
|
||||
|
if (result.Errors != null && result.Errors.Count > 0) |
||||
|
{ |
||||
|
var errors = result.Errors.Select(x => new ValidationError(x.Message)).ToArray(); |
||||
|
|
||||
|
throw new ValidationException("Failed to execute GraphQL query.", errors); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public IFieldPartitioning ResolvePartition(Partitioning key) |
||||
|
{ |
||||
|
return partitionResolver(key); |
||||
|
} |
||||
|
|
||||
|
public IGraphType GetAssetType() |
||||
|
{ |
||||
|
return assetType; |
||||
|
} |
||||
|
|
||||
|
public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) |
||||
|
{ |
||||
|
return fieldInfos[field.GetType()](field); |
||||
|
} |
||||
|
|
||||
|
public IGraphType GetSchemaType(Guid schemaId) |
||||
|
{ |
||||
|
return schemaTypes.GetOrAdd(schemaId, k => |
||||
|
{ |
||||
|
var schemaEntity = schemas.GetOrDefault(k); |
||||
|
|
||||
|
return schemaEntity != null ? new ContentGraphType(schemaEntity.Schema, this) : null; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphQLQuery.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
||||
|
{ |
||||
|
public class GraphQLQuery |
||||
|
{ |
||||
|
public string OperationName { get; set; } |
||||
|
|
||||
|
public string NamedQuery { get; set; } |
||||
|
|
||||
|
public string Query { get; set; } |
||||
|
|
||||
|
public string Variables { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IGraphQLInvoker.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Read.Apps; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
||||
|
{ |
||||
|
public interface IGraphQLInvoker |
||||
|
{ |
||||
|
Task<object> QueryAsync(IAppEntity appEntity, GraphQLQuery query); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,183 @@ |
|||||
|
// ==========================================================================
|
||||
|
// QueryContext.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Squidex.Domain.Apps.Read.Contents.Repositories; |
||||
|
using Squidex.Infrastructure; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Read.Apps; |
||||
|
using Squidex.Domain.Apps.Read.Assets; |
||||
|
using Squidex.Domain.Apps.Read.Assets.Repositories; |
||||
|
|
||||
|
// ReSharper disable InvertIf
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
||||
|
{ |
||||
|
public sealed class QueryContext |
||||
|
{ |
||||
|
private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>(); |
||||
|
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>(); |
||||
|
private readonly IContentRepository contentRepository; |
||||
|
private readonly IAssetRepository assetRepository; |
||||
|
private readonly IAppEntity appEntity; |
||||
|
|
||||
|
public QueryContext(IAppEntity appEntity, IContentRepository contentRepository, IAssetRepository assetRepository) |
||||
|
{ |
||||
|
Guard.NotNull(contentRepository, nameof(contentRepository)); |
||||
|
Guard.NotNull(assetRepository, nameof(assetRepository)); |
||||
|
Guard.NotNull(appEntity, nameof(appEntity)); |
||||
|
|
||||
|
this.contentRepository = contentRepository; |
||||
|
this.assetRepository = assetRepository; |
||||
|
|
||||
|
this.appEntity = appEntity; |
||||
|
} |
||||
|
|
||||
|
public async Task<IAssetEntity> FindAssetAsync(Guid id) |
||||
|
{ |
||||
|
var asset = cachedAssets.GetOrDefault(id); |
||||
|
|
||||
|
if (asset == null) |
||||
|
{ |
||||
|
asset = await assetRepository.FindAssetAsync(id); |
||||
|
|
||||
|
if (asset != null) |
||||
|
{ |
||||
|
cachedAssets[asset.Id] = asset; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return asset; |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id) |
||||
|
{ |
||||
|
var content = cachedContents.GetOrDefault(id); |
||||
|
|
||||
|
if (content == null) |
||||
|
{ |
||||
|
content = await contentRepository.FindContentAsync(appEntity, schemaId, id); |
||||
|
|
||||
|
if (content != null) |
||||
|
{ |
||||
|
cachedContents[content.Id] = content; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IAssetEntity>> QueryAssetsAsync(string query, int skip = 0, int take = 10) |
||||
|
{ |
||||
|
var assets = await assetRepository.QueryAsync(appEntity.Id, null, null, query, take, skip); |
||||
|
|
||||
|
foreach (var asset in assets) |
||||
|
{ |
||||
|
cachedAssets[asset.Id] = asset; |
||||
|
} |
||||
|
|
||||
|
return assets; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IContentEntity>> QueryContentsAsync(Guid schemaId, string query) |
||||
|
{ |
||||
|
var contents = await contentRepository.QueryAsync(appEntity, schemaId, false, null, query); |
||||
|
|
||||
|
foreach (var content in contents) |
||||
|
{ |
||||
|
cachedContents[content.Id] = content; |
||||
|
} |
||||
|
|
||||
|
return contents; |
||||
|
} |
||||
|
|
||||
|
public List<IAssetEntity> GetReferencedAssets(JToken value) |
||||
|
{ |
||||
|
var ids = ParseIds(value); |
||||
|
|
||||
|
return GetReferencedAssets(ids); |
||||
|
} |
||||
|
|
||||
|
public List<IAssetEntity> GetReferencedAssets(ICollection<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(ids, nameof(ids)); |
||||
|
|
||||
|
var notLoadedAssets = new HashSet<Guid>(ids.Where(id => !cachedAssets.ContainsKey(id))); |
||||
|
|
||||
|
if (notLoadedAssets.Count > 0) |
||||
|
{ |
||||
|
Task.Run(async () => |
||||
|
{ |
||||
|
var assets = await assetRepository.QueryAsync(appEntity.Id, null, notLoadedAssets, string.Empty, int.MaxValue).ConfigureAwait(false); |
||||
|
|
||||
|
foreach (var asset in assets) |
||||
|
{ |
||||
|
cachedAssets[asset.Id] = asset; |
||||
|
} |
||||
|
}).Wait(); |
||||
|
} |
||||
|
|
||||
|
return ids.Select(id => cachedAssets.GetOrDefault(id)).Where(x => x != null).ToList(); |
||||
|
} |
||||
|
|
||||
|
public List<IContentEntity> GetReferencedContents(Guid schemaId, JToken value) |
||||
|
{ |
||||
|
var ids = ParseIds(value); |
||||
|
|
||||
|
return GetReferencedContents(schemaId, ids); |
||||
|
} |
||||
|
|
||||
|
public List<IContentEntity> GetReferencedContents(Guid schemaId, ICollection<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(ids, nameof(ids)); |
||||
|
|
||||
|
var notLoadedContents = new HashSet<Guid>(ids.Where(id => !cachedContents.ContainsKey(id))); |
||||
|
|
||||
|
if (notLoadedContents.Count > 0) |
||||
|
{ |
||||
|
Task.Run(async () => |
||||
|
{ |
||||
|
var contents = await contentRepository.QueryAsync(appEntity, schemaId, false, notLoadedContents, string.Empty).ConfigureAwait(false); |
||||
|
|
||||
|
foreach (var content in contents) |
||||
|
{ |
||||
|
cachedContents[content.Id] = content; |
||||
|
} |
||||
|
}).Wait(); |
||||
|
} |
||||
|
|
||||
|
return ids.Select(id => cachedContents.GetOrDefault(id)).Where(x => x != null).ToList(); |
||||
|
} |
||||
|
|
||||
|
private static ICollection<Guid> ParseIds(JToken value) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var result = new List<Guid>(); |
||||
|
|
||||
|
if (value is JArray) |
||||
|
{ |
||||
|
foreach (var id in value) |
||||
|
{ |
||||
|
result.Add(Guid.Parse(id.ToString())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return new List<Guid>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AssetGraphType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Read.Assets; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity> |
||||
|
{ |
||||
|
public AssetGraphType() |
||||
|
{ |
||||
|
Name = "AssetDto"; |
||||
|
|
||||
|
Field("id", x => x.Id.ToString()) |
||||
|
.Description("The id of the asset."); |
||||
|
|
||||
|
Field("version", x => x.Version) |
||||
|
.Description("The version of the asset."); |
||||
|
|
||||
|
Field("created", x => x.Created.ToDateTimeUtc()) |
||||
|
.Description("The date and time when the asset has been created."); |
||||
|
|
||||
|
Field("createdBy", x => x.CreatedBy.ToString()) |
||||
|
.Description("The user that has created the asset."); |
||||
|
|
||||
|
Field("lastModified", x => x.LastModified.ToDateTimeUtc()) |
||||
|
.Description("The date and time when the asset has been modified last."); |
||||
|
|
||||
|
Field("lastModifiedBy", x => x.LastModifiedBy.ToString()) |
||||
|
.Description("The user that has updated the asset last."); |
||||
|
|
||||
|
Field("mimeType", x => x.MimeType) |
||||
|
.Description("The mime type."); |
||||
|
|
||||
|
Field("fileName", x => x.FileName) |
||||
|
.Description("The file name."); |
||||
|
|
||||
|
Field("fileSize", x => x.FileSize) |
||||
|
.Description("The size of the file in bytes."); |
||||
|
|
||||
|
Field("fileVersion", x => x.FileVersion) |
||||
|
.Description("The version of the file."); |
||||
|
|
||||
|
Field("isImage", x => x.IsImage) |
||||
|
.Description("Determines of the created file is an image."); |
||||
|
|
||||
|
Field("pixelWidth", x => x.PixelWidth, true) |
||||
|
.Description("The width of the image in pixels if the asset is an image."); |
||||
|
|
||||
|
Field("pixelHeight", x => x.PixelHeight, true) |
||||
|
.Description("The height of the image in pixels if the asset is an image."); |
||||
|
|
||||
|
Description = "An asset"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentDataGraphType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
||||
|
|
||||
|
// ReSharper disable InvertIf
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> |
||||
|
{ |
||||
|
private static readonly IFieldResolver FieldResolver = |
||||
|
new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(c.FieldName)); |
||||
|
|
||||
|
public ContentDataGraphType(Schema schema, IGraphQLContext graphQLContext) |
||||
|
{ |
||||
|
var schemaName = schema.Properties.Label.WithFallback(schema.Name); |
||||
|
|
||||
|
Name = $"{schema.Name.ToPascalCase()}DataDto"; |
||||
|
|
||||
|
foreach (var field in schema.Fields) |
||||
|
{ |
||||
|
var fieldInfo = graphQLContext.GetGraphType(field); |
||||
|
|
||||
|
if (fieldInfo.ResolveType != null) |
||||
|
{ |
||||
|
var fieldName = field.RawProperties.Label.WithFallback(field.Name); |
||||
|
var fieldGraphType = new ObjectGraphType |
||||
|
{ |
||||
|
Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" |
||||
|
}; |
||||
|
|
||||
|
var partition = graphQLContext.ResolvePartition(field.Paritioning); |
||||
|
|
||||
|
foreach (var partitionItem in partition) |
||||
|
{ |
||||
|
fieldGraphType.AddField(new FieldType |
||||
|
{ |
||||
|
Name = partitionItem.Key, |
||||
|
Resolver = fieldInfo.Resolver, |
||||
|
ResolvedType = fieldInfo.ResolveType, |
||||
|
Description = field.RawProperties.Hints |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type."; |
||||
|
|
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = field.Name, |
||||
|
Resolver = FieldResolver, |
||||
|
ResolvedType = fieldGraphType |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Description = $"The structure of a {schemaName} content type."; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,192 @@ |
|||||
|
// ==========================================================================
|
||||
|
// GraphModelType.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using GraphQL.Resolvers; |
||||
|
using GraphQL.Types; |
||||
|
using Squidex.Domain.Apps.Read.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types |
||||
|
{ |
||||
|
public sealed class ContentQueryType : ObjectGraphType |
||||
|
{ |
||||
|
public ContentQueryType(IGraphQLContext graphQLContext, IEnumerable<ISchemaEntity> schemaEntities) |
||||
|
{ |
||||
|
AddAssetFind(graphQLContext); |
||||
|
AddAssetsQuery(graphQLContext); |
||||
|
|
||||
|
foreach (var schemaEntity in schemaEntities) |
||||
|
{ |
||||
|
var schemaName = schemaEntity.Schema.Properties.Label.WithFallback(schemaEntity.Schema.Name); |
||||
|
var schemaType = new ContentGraphType(schemaEntity.Schema, graphQLContext); |
||||
|
|
||||
|
AddContentFind(schemaEntity, schemaName, schemaType); |
||||
|
AddContentQuery(schemaEntity, schemaType, schemaName); |
||||
|
} |
||||
|
|
||||
|
Description = "The app queries."; |
||||
|
} |
||||
|
|
||||
|
private void AddAssetFind(IGraphQLContext graphQLContext) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "findAsset", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "id", |
||||
|
Description = "The id of the asset.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = graphQLContext.GetAssetType(), |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (QueryContext)c.UserContext; |
||||
|
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
||||
|
|
||||
|
return context.FindAssetAsync(contentId); |
||||
|
}), |
||||
|
Description = "Find an asset by id." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void AddContentFind(ISchemaEntity schemaEntity, string schemaName, IGraphType schemaType) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = $"find{schemaEntity.Name.ToPascalCase()}", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "id", |
||||
|
Description = $"The id of the {schemaName} content.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = schemaType, |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (QueryContext)c.UserContext; |
||||
|
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
||||
|
|
||||
|
return context.FindContentAsync(schemaEntity.Id, contentId); |
||||
|
}), |
||||
|
Description = $"Find an {schemaName} content by id." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void AddAssetsQuery(IGraphQLContext graphQLContext) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = "queryAssets", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "top", |
||||
|
Description = "Optional number of assets to take.", |
||||
|
DefaultValue = 20 |
||||
|
}, |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "skip", |
||||
|
Description = "Optional number of assets to skip.", |
||||
|
DefaultValue = 0 |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "search", |
||||
|
Description = "Optional query.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = new ListGraphType(new NonNullGraphType(graphQLContext.GetAssetType())), |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (QueryContext)c.UserContext; |
||||
|
|
||||
|
var argTop = c.GetArgument("top", 20); |
||||
|
var argSkip = c.GetArgument("skip", 0); |
||||
|
var argQuery = c.GetArgument("query", string.Empty); |
||||
|
|
||||
|
return context.QueryAssetsAsync(argQuery, argSkip, argTop); |
||||
|
}), |
||||
|
Description = "Query assets items." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void AddContentQuery(ISchemaEntity schemaEntity, IGraphType schemaType, string schemaName) |
||||
|
{ |
||||
|
AddField(new FieldType |
||||
|
{ |
||||
|
Name = $"query{schemaEntity.Name.ToPascalCase()}", |
||||
|
Arguments = new QueryArguments |
||||
|
{ |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "top", |
||||
|
Description = "Optional number of contents to take.", |
||||
|
DefaultValue = 20 |
||||
|
}, |
||||
|
new QueryArgument(typeof(IntGraphType)) |
||||
|
{ |
||||
|
Name = "skip", |
||||
|
Description = "Optional number of contents to skip.", |
||||
|
DefaultValue = 0 |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "filter", |
||||
|
Description = "Optional OData filter.", |
||||
|
DefaultValue = string.Empty |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "search", |
||||
|
Description = "Optional OData full text search.", |
||||
|
DefaultValue = string.Empty |
||||
|
}, |
||||
|
new QueryArgument(typeof(StringGraphType)) |
||||
|
{ |
||||
|
Name = "orderby", |
||||
|
Description = "Optional OData order definition.", |
||||
|
DefaultValue = string.Empty |
||||
|
} |
||||
|
}, |
||||
|
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), |
||||
|
Resolver = new FuncFieldResolver<object>(c => |
||||
|
{ |
||||
|
var context = (QueryContext)c.UserContext; |
||||
|
var contentQuery = BuildODataQuery(c); |
||||
|
|
||||
|
return context.QueryContentsAsync(schemaEntity.Id, contentQuery); |
||||
|
}), |
||||
|
Description = $"Query {schemaName} content items." |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private static string BuildODataQuery(ResolveFieldContext c) |
||||
|
{ |
||||
|
var odataQuery = "?" + |
||||
|
string.Join("&", |
||||
|
c.Arguments |
||||
|
.Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
||||
|
.Select(x => $"${x.Key}={x.Value}")); |
||||
|
|
||||
|
return odataQuery; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,37 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// SchemaGraphType.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using GraphQL.Resolvers; |
|
||||
using GraphQL.Types; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.GraphQl |
|
||||
{ |
|
||||
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> |
|
||||
{ |
|
||||
private static readonly IFieldResolver FieldResolver = |
|
||||
new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(c.FieldName)); |
|
||||
|
|
||||
public ContentDataGraphType(Schema schema, IGraphQLContext context) |
|
||||
{ |
|
||||
foreach (var field in schema.Fields) |
|
||||
{ |
|
||||
var fieldName = field.Name; |
|
||||
|
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = fieldName, |
|
||||
Resolver = FieldResolver, |
|
||||
ResolvedType = new SchemaFieldGraphType(field, context), |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,36 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// SchemaGraphType.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using GraphQL.Types; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.GraphQl |
|
||||
{ |
|
||||
|
|
||||
public sealed class ContentFieldGraphType : ObjectGraphType<ContentFieldData> |
|
||||
{ |
|
||||
public ContentFieldGraphType(Field field, IGraphQLContext context) |
|
||||
{ |
|
||||
var partition = context.ResolvePartition(field.Paritioning); |
|
||||
|
|
||||
foreach (var partitionItem in partition) |
|
||||
{ |
|
||||
var fieldInfo = context.GetGraphType(field); |
|
||||
|
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = partitionItem.Key, |
|
||||
Resolver = fieldInfo.Resolver, |
|
||||
ResolvedType = fieldInfo.ResolveType, |
|
||||
Description = field.RawProperties.Hints |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,94 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// GraphQLContext.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Concurrent; |
|
||||
using System.Collections.Generic; |
|
||||
using GraphQL.Resolvers; |
|
||||
using GraphQL.Types; |
|
||||
using Squidex.Domain.Apps.Core; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Domain.Apps.Read.Contents; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.GraphQl |
|
||||
{ |
|
||||
public sealed class GraphQLContext : IGraphQLContext |
|
||||
{ |
|
||||
private readonly PartitionResolver partitionResolver; |
|
||||
private readonly Dictionary<Type, (IGraphType ResolveType, IFieldResolver Resolver)> fieldInfos; |
|
||||
|
|
||||
public GraphQLContext(PartitionResolver partitionResolver) |
|
||||
{ |
|
||||
Guard.NotNull(partitionResolver, nameof(partitionResolver)); |
|
||||
|
|
||||
this.partitionResolver = partitionResolver; |
|
||||
|
|
||||
var defaultResolver = |
|
||||
new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName)); |
|
||||
|
|
||||
fieldInfos = new Dictionary<Type, (IGraphType ResolveType, IFieldResolver Resolver)> |
|
||||
{ |
|
||||
{ |
|
||||
typeof(StringField), |
|
||||
(new StringGraphType(), defaultResolver) |
|
||||
}, |
|
||||
{ |
|
||||
typeof(BooleanField), |
|
||||
(new BooleanGraphType(), defaultResolver) |
|
||||
}, |
|
||||
{ |
|
||||
typeof(NumberField), |
|
||||
(new FloatGraphType(), defaultResolver) |
|
||||
}, |
|
||||
{ |
|
||||
typeof(DateTimeField), |
|
||||
(new FloatGraphType(), defaultResolver) |
|
||||
}, |
|
||||
{ |
|
||||
typeof(JsonField), |
|
||||
(new ObjectGraphType(), defaultResolver) |
|
||||
}, |
|
||||
{ |
|
||||
typeof(GeolocationField), |
|
||||
(new ObjectGraphType(), defaultResolver) |
|
||||
}, |
|
||||
{ |
|
||||
typeof(AssetsField), |
|
||||
(new ListGraphType<StringGraphType>(), defaultResolver) |
|
||||
}, |
|
||||
{ |
|
||||
typeof(ReferencesField), |
|
||||
(new ListGraphType<StringGraphType>(), defaultResolver) |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
public IGraphType GetSchemaListType(Schema schema) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGraphType GetSchemaListType(Guid schemaId) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IFieldPartitioning ResolvePartition(Partitioning key) |
|
||||
{ |
|
||||
return partitionResolver(key); |
|
||||
} |
|
||||
|
|
||||
public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) |
|
||||
{ |
|
||||
return fieldInfos[field.GetType()]; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,33 @@ |
|||||
|
<sqx-title message="{app} | Settings" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
||||
|
|
||||
|
<sqx-panel theme="dark" panelWidth="12rem"> |
||||
|
<div class="panel-header"> |
||||
|
<div class="panel-title-row"> |
||||
|
<h3 class="panel-title">API</h3> |
||||
|
</div> |
||||
|
|
||||
|
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
||||
|
<i class="icon-close"></i> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<div class="panel-main"> |
||||
|
<div class="panel-content"> |
||||
|
<ul class="nav flex-column nav-dark"> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> |
||||
|
GraphQL |
||||
|
<i class="icon-angle-right"></i> |
||||
|
</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link"> |
||||
|
REST |
||||
|
</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</sqx-panel> |
||||
|
|
||||
|
<router-outlet></router-outlet> |
||||
@ -0,0 +1,12 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.nav-link { |
||||
|
position: relative; |
||||
|
padding-top: .6rem; |
||||
|
padding-bottom: .6rem; |
||||
|
} |
||||
|
|
||||
|
.icon-angle-right { |
||||
|
@include absolute(14px, 2rem, auto, auto); |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
import { |
||||
|
AppComponentBase, |
||||
|
AppsStoreService, |
||||
|
NotificationService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-api-area', |
||||
|
styleUrls: ['./api-area.component.scss'], |
||||
|
templateUrl: './api-area.component.html' |
||||
|
}) |
||||
|
export class ApiAreaComponent extends AppComponentBase { |
||||
|
constructor(apps: AppsStoreService, notifications: NotificationService |
||||
|
) { |
||||
|
super(notifications, apps); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
export * from './pages/graphql/graphql-page.component'; |
||||
|
|
||||
|
export * from './api-area.component'; |
||||
@ -0,0 +1,9 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
export * from './declarations'; |
||||
|
export * from './module'; |
||||
@ -0,0 +1,50 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { RouterModule, Routes } from '@angular/router'; |
||||
|
import { DndModule } from 'ng2-dnd'; |
||||
|
|
||||
|
import { |
||||
|
SqxFrameworkModule, |
||||
|
SqxSharedModule |
||||
|
} from 'shared'; |
||||
|
|
||||
|
import { |
||||
|
ApiAreaComponent, |
||||
|
GraphQLPageComponent |
||||
|
} from './declarations'; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: '', |
||||
|
component: ApiAreaComponent, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: '' |
||||
|
}, |
||||
|
{ |
||||
|
path: 'graphql', |
||||
|
component: GraphQLPageComponent |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
DndModule, |
||||
|
SqxFrameworkModule, |
||||
|
SqxSharedModule, |
||||
|
RouterModule.forChild(routes) |
||||
|
], |
||||
|
declarations: [ |
||||
|
ApiAreaComponent, |
||||
|
GraphQLPageComponent |
||||
|
] |
||||
|
}) |
||||
|
export class SqxFeatureApiModule { } |
||||
@ -0,0 +1,5 @@ |
|||||
|
<sqx-title message="{app} | API | GraphQL" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
||||
|
|
||||
|
<sqx-panel panelWidth="60rem" expand="true"> |
||||
|
<div #graphiQLContainer></div> |
||||
|
</sqx-panel> |
||||
@ -0,0 +1,12 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
@import '~graphiql/graphiql'; |
||||
|
|
||||
|
.graphiql-container { |
||||
|
@include absolute(0, 0, 0, 0); |
||||
|
} |
||||
|
|
||||
|
.graphiql-container > * { |
||||
|
box-sizing: content-box; |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; |
||||
|
|
||||
|
import * as React from 'react'; |
||||
|
import * as ReactDOM from 'react-dom'; |
||||
|
|
||||
|
const GraphiQL = require('graphiql'); |
||||
|
|
||||
|
import { |
||||
|
ApiUrlConfig, |
||||
|
AppComponentBase, |
||||
|
AppsStoreService, |
||||
|
AuthService, |
||||
|
NotificationService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-graphql-page', |
||||
|
styleUrls: ['./graphql-page.component.scss'], |
||||
|
templateUrl: './graphql-page.component.html', |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class GraphQLPageComponent extends AppComponentBase implements OnInit { |
||||
|
@ViewChild('graphiQLContainer') |
||||
|
public graphiQLContainer: ElementRef; |
||||
|
|
||||
|
constructor(apps: AppsStoreService, notifications: NotificationService, |
||||
|
private readonly authService: AuthService, |
||||
|
private readonly apiUrl: ApiUrlConfig |
||||
|
) { |
||||
|
super(notifications, apps); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
ReactDOM.render( |
||||
|
React.createElement(GraphiQL, { |
||||
|
fetcher: (params: any) => this.request(params) |
||||
|
}), |
||||
|
this.graphiQLContainer.nativeElement |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private request(params: any) { |
||||
|
return this.appNameOnce() |
||||
|
.switchMap(app => this.authService.authPost(this.apiUrl.buildUrl(`api/content/${app}/graphql`), params).map(r => r.json())) |
||||
|
.toPromise(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -1,84 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { PanelService, PanelServiceFactory } from './../'; |
|
||||
|
|
||||
interface Styling { element: any; property: string; value: string; } |
|
||||
|
|
||||
describe('PanelService', () => { |
|
||||
it('should instantiate from factory', () => { |
|
||||
const panelService = PanelServiceFactory(); |
|
||||
|
|
||||
expect(panelService).toBeDefined(); |
|
||||
}); |
|
||||
|
|
||||
it('should instantiate', () => { |
|
||||
const panelService = new PanelService(); |
|
||||
|
|
||||
expect(panelService).toBeDefined(); |
|
||||
}); |
|
||||
|
|
||||
it('should update elements with renderer service', () => { |
|
||||
let styles: Styling[] = []; |
|
||||
|
|
||||
const renderer = { |
|
||||
setElementStyle: (element: any, property: string, value: string) => { |
|
||||
styles.push({element, property, value}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const panelService = new PanelService(); |
|
||||
|
|
||||
const element1 = { |
|
||||
getBoundingClientRect: () => { |
|
||||
return { width: 100 }; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const element2 = { |
|
||||
getBoundingClientRect: () => { |
|
||||
return { width: 200 }; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const element3 = { |
|
||||
getBoundingClientRect: () => { |
|
||||
return { width: 300 }; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
let numPublished = 0; |
|
||||
panelService.changed.subscribe(() => { |
|
||||
numPublished++; |
|
||||
}); |
|
||||
|
|
||||
panelService.push(element1); |
|
||||
panelService.push(element2); |
|
||||
panelService.push(element3); |
|
||||
|
|
||||
styles = []; |
|
||||
|
|
||||
panelService.pop(element3); |
|
||||
panelService.render(<any>renderer); |
|
||||
|
|
||||
expect(styles).toEqual([ |
|
||||
{ element: element1, property: 'top', value: '0px' }, |
|
||||
{ element: element1, property: 'left', value: '0px' }, |
|
||||
{ element: element1, property: 'bottom', value: '0px' }, |
|
||||
{ element: element1, property: 'position', value: 'absolute' }, |
|
||||
{ element: element1, property: 'z-index', value: '20' }, |
|
||||
|
|
||||
{ element: element2, property: 'top', value: '0px' }, |
|
||||
{ element: element2, property: 'left', value: '100px' }, |
|
||||
{ element: element2, property: 'bottom', value: '0px' }, |
|
||||
{ element: element2, property: 'position', value: 'absolute' }, |
|
||||
{ element: element2, property: 'z-index', value: '10' } |
|
||||
]); |
|
||||
|
|
||||
expect(numPublished).toBe(1); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,51 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Injectable, Renderer } from '@angular/core'; |
|
||||
import { Observable, Subject } from 'rxjs'; |
|
||||
|
|
||||
export const PanelServiceFactory = () => { |
|
||||
return new PanelService(); |
|
||||
}; |
|
||||
|
|
||||
@Injectable() |
|
||||
export class PanelService { |
|
||||
private readonly elements: any[] = []; |
|
||||
private readonly changed$ = new Subject<number>(); |
|
||||
|
|
||||
public get changed(): Observable<number> { |
|
||||
return this.changed$; |
|
||||
} |
|
||||
|
|
||||
public push(element: any) { |
|
||||
this.elements.push(element); |
|
||||
} |
|
||||
|
|
||||
public pop(element: any) { |
|
||||
this.elements.splice(-1, 1); |
|
||||
} |
|
||||
|
|
||||
public render(renderer: Renderer) { |
|
||||
let currentPosition = 0; |
|
||||
let currentLayer = this.elements.length * 10; |
|
||||
|
|
||||
for (let element of this.elements) { |
|
||||
const width = element.getBoundingClientRect().width; |
|
||||
|
|
||||
renderer.setElementStyle(element, 'top', '0px'); |
|
||||
renderer.setElementStyle(element, 'left', currentPosition + 'px'); |
|
||||
renderer.setElementStyle(element, 'bottom', '0px'); |
|
||||
renderer.setElementStyle(element, 'position', 'absolute'); |
|
||||
renderer.setElementStyle(element, 'z-index', currentLayer.toString()); |
|
||||
|
|
||||
currentPosition += width; |
|
||||
currentLayer -= 10; |
|
||||
} |
|
||||
|
|
||||
this.changed$.next(currentPosition); |
|
||||
} |
|
||||
} |
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue