Browse Source

Provide URLs in GraphQL endpoint. Closes #79

pull/85/head
Sebastian Stehle 9 years ago
parent
commit
8f5f69dbb1
  1. 9
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLInvoker.cs
  2. 53
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs
  3. 7
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs
  4. 23
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs
  5. 10
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs
  6. 18
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs
  7. 3
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs
  8. 22
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs
  9. 5
      src/Squidex/Config/Domain/ReadModule.cs
  10. 50
      src/Squidex/Pipeline/GraphQLUrlGenerator.cs
  11. 2
      src/Squidex/app/framework/angular/image-source.directive.ts
  12. 14
      tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs
  13. 33
      tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs

9
src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLInvoker.cs

@ -27,6 +27,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
public sealed class CachingGraphQLInvoker : CachingProviderBase, IGraphQLInvoker, IEventConsumer
{
private readonly IContentRepository contentRepository;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository;
private readonly ISchemaRepository schemaRepository;
@ -40,16 +41,18 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
get { return "^(schema-)|(apps-)"; }
}
public CachingGraphQLInvoker(IMemoryCache cache, ISchemaRepository schemaRepository, IAssetRepository assetRepository, IContentRepository contentRepository)
public CachingGraphQLInvoker(IMemoryCache cache, ISchemaRepository schemaRepository, IAssetRepository assetRepository, IContentRepository contentRepository, IGraphQLUrlGenerator urlGenerator)
: base(cache)
{
Guard.NotNull(schemaRepository, nameof(schemaRepository));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.contentRepository = contentRepository;
this.schemaRepository = schemaRepository;
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
this.urlGenerator = urlGenerator;
}
public Task ClearAsync()
@ -73,7 +76,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
Guard.NotNull(query, nameof(query));
var modelContext = await GetModelAsync(app);
var queryContext = new QueryContext(app, contentRepository, assetRepository);
var queryContext = new QueryContext(app, contentRepository, assetRepository, urlGenerator);
return await modelContext.ExecuteAsync(queryContext, query);
}

53
src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs

@ -17,11 +17,13 @@ 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.Assets;
using Squidex.Domain.Apps.Read.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Infrastructure;
using GraphQLSchema = GraphQL.Types.Schema;
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
// ReSharper disable ConvertClosureToMethodGroup
// ReSharper disable InvertIf
// ReSharper disable ParameterHidesMember
@ -34,14 +36,19 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
private readonly Dictionary<Guid, ContentGraphType> schemaTypes = new Dictionary<Guid, ContentGraphType>();
private readonly Dictionary<Guid, ISchemaEntity> schemas;
private readonly PartitionResolver partitionResolver;
private readonly IGraphType assetType = new AssetGraphType();
private readonly IAppEntity appEntity;
private readonly IGraphType assetType;
private readonly IGraphType assetListType;
private readonly GraphQLSchema graphQLSchema;
public GraphQLModel(IAppEntity app, IEnumerable<ISchemaEntity> schemas)
public GraphQLModel(IAppEntity appEntity, IEnumerable<ISchemaEntity> schemas)
{
partitionResolver = app.PartitionResolver;
this.appEntity = appEntity;
partitionResolver = appEntity.PartitionResolver;
IGraphType assetListType = new ListGraphType(new NonNullGraphType(assetType));
assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType));
fieldInfos = new Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>>
{
@ -94,6 +101,42 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
return (new NoopGraphType(name), new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName)));
}
public IFieldResolver ResolveAssetUrl()
{
var resolver = new FuncFieldResolver<IAssetEntity, object>(c =>
{
var context = (QueryContext)c.UserContext;
return context.UrlGenerator.GenerateAssetUrl(appEntity, c.Source);
});
return resolver;
}
public IFieldResolver ResolveAssetThumbnailUrl()
{
var resolver = new FuncFieldResolver<IAssetEntity, object>(c =>
{
var context = (QueryContext)c.UserContext;
return context.UrlGenerator.GenerateAssetThumbnailUrl(appEntity, c.Source);
});
return resolver;
}
public IFieldResolver ResolveContentUrl(ISchemaEntity schemaEntity)
{
var resolver = new FuncFieldResolver<IContentEntity, object>(c =>
{
var context = (QueryContext)c.UserContext;
return context.UrlGenerator.GenerateContentUrl(appEntity, schemaEntity, c.Source);
});
return resolver;
}
private static ValueTuple<IGraphType, IFieldResolver> ResolveAssets(IGraphType assetListType)
{
var resolver = new FuncFieldResolver<ContentFieldData, object>(c =>
@ -172,7 +215,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
{
var schemaEntity = schemas.GetOrDefault(schemaId);
return schemaEntity != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schemaEntity.Schema, this)) : null;
return schemaEntity != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schemaEntity, this)) : null;
}
}
}

7
src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs

@ -11,6 +11,7 @@ using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.Schemas;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL
{
@ -22,6 +23,12 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
IGraphType GetSchemaType(Guid schemaId);
IFieldResolver ResolveAssetUrl();
IFieldResolver ResolveAssetThumbnailUrl();
IFieldResolver ResolveContentUrl(ISchemaEntity schemaEntity);
(IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field);
}
}

23
src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs

@ -0,0 +1,23 @@
// ==========================================================================
// IGraphQLUrlGenerator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Schemas;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL
{
public interface IGraphQLUrlGenerator
{
string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity);
string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity);
string GenerateContentUrl(IAppEntity appEntity, ISchemaEntity schemaEntity, IContentEntity contentEntity);
}
}

10
src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs

@ -28,16 +28,24 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>();
private readonly IContentRepository contentRepository;
private readonly IAssetRepository assetRepository;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAppEntity app;
public QueryContext(IAppEntity app, IContentRepository contentRepository, IAssetRepository assetRepository)
public IGraphQLUrlGenerator UrlGenerator
{
get { return urlGenerator; }
}
public QueryContext(IAppEntity app, IContentRepository contentRepository, IAssetRepository assetRepository, IGraphQLUrlGenerator urlGenerator)
{
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
Guard.NotNull(app, nameof(app));
this.contentRepository = contentRepository;
this.assetRepository = assetRepository;
this.urlGenerator = urlGenerator;
this.app = app;
}

18
src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
{
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity>
{
public AssetGraphType()
public AssetGraphType(IGraphQLContext context)
{
Name = "AssetDto";
@ -75,6 +75,22 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
Description = "The mime type."
});
AddField(new FieldType
{
Name = "url",
Resolver = context.ResolveAssetUrl(),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Description = "The url to the asset."
});
AddField(new FieldType
{
Name = "thumbnailUrl",
Resolver = context.ResolveAssetThumbnailUrl(),
ResolvedType = new StringGraphType(),
Description = "The thumbnail url to the asset."
});
AddField(new FieldType
{
Name = "fileName",

3
src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -52,8 +52,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type.";
var fieldResolver =
new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(field.Name));
var fieldResolver = new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(field.Name));
AddField(new FieldType
{

22
src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs

@ -9,27 +9,27 @@
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Infrastructure;
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
{
public sealed class ContentGraphType : ObjectGraphType<IContentEntity>
{
private readonly Schema schema;
private readonly ISchemaEntity schemaEntity;
private readonly IGraphQLContext context;
public ContentGraphType(Schema schema, IGraphQLContext context)
public ContentGraphType(ISchemaEntity schemaEntity, IGraphQLContext context)
{
this.schema = schema;
this.context = context;
this.schemaEntity = schemaEntity;
Name = $"{schema.Name.ToPascalCase()}Dto";
Name = $"{schemaEntity.Name.ToPascalCase()}Dto";
}
public void Initialize()
{
var schemaName = schema.Properties.Label.WithFallback(schema.Name);
var schemaName = schemaEntity.Schema.Properties.Label.WithFallback(schemaEntity.Name);
AddField(new FieldType
{
@ -79,11 +79,19 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
Description = $"The user that has updated the {schemaName} content last."
});
AddField(new FieldType
{
Name = "url",
Resolver = context.ResolveContentUrl(schemaEntity),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Description = $"The url to the the {schemaName} content."
});
AddField(new FieldType
{
Name = "data",
Resolver = Resolver(x => x.Data),
ResolvedType = new NonNullGraphType(new ContentDataGraphType(schema, context)),
ResolvedType = new NonNullGraphType(new ContentDataGraphType(schemaEntity.Schema, context)),
Description = $"The data of the {schemaName} content."
});

5
src/Squidex/Config/Domain/ReadModule.cs

@ -23,6 +23,7 @@ using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Users;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Pipeline;
// ReSharper disable UnusedAutoPropertyAccessor.Local
@ -64,6 +65,10 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.RegisterType<GraphQLUrlGenerator>()
.As<IGraphQLUrlGenerator>()
.SingleInstance();
builder.RegisterType<AssetUserPictureStore>()
.As<IUserPictureStore>()
.SingleInstance();

50
src/Squidex/Pipeline/GraphQLUrlGenerator.cs

@ -0,0 +1,50 @@
// ==========================================================================
// GraphQLUrlGenerator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.Schemas;
// ReSharper disable ConvertIfStatementToReturnStatement
namespace Squidex.Pipeline
{
public sealed class GraphQLUrlGenerator : IGraphQLUrlGenerator
{
private readonly MyUrlsOptions urlsOptions;
public GraphQLUrlGenerator(IOptions<MyUrlsOptions> urlsOptions)
{
this.urlsOptions = urlsOptions.Value;
}
public string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity)
{
if (!assetEntity.IsImage)
{
return null;
}
return urlsOptions.BuildUrl($"api/assets/{assetEntity.Id}?version={assetEntity.Version}&width=100&mode=Max");
}
public string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity)
{
return urlsOptions.BuildUrl($"api/assets/{assetEntity.Id}?version={assetEntity.Version}");
}
public string GenerateContentUrl(IAppEntity appEntity, ISchemaEntity schemaEntity, IContentEntity contentEntity)
{
return urlsOptions.BuildUrl($"api/content/{appEntity.Name}/{schemaEntity.Name}/{contentEntity.Id}");
}
}
}

2
src/Squidex/app/framework/angular/image-source.directive.ts

@ -92,7 +92,7 @@ export class ImageSourceDirective implements OnChanges, OnInit, AfterViewInit {
let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`;
if (this.query !== null) {
source += `q=${this.query}`;
source += `&q=${this.query}`;
}
this.renderer.setElementAttribute(this.element.nativeElement, 'src', source);

14
tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs

@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Read.Contents
schemaRepository.Setup(x => x.QueryAllAsync(appId)).Returns(Task.FromResult<IReadOnlyList<ISchemaEntity>>(schemas));
sut = new CachingGraphQLInvoker(cache, schemaRepository.Object, assetRepository.Object, contentRepository.Object);
sut = new CachingGraphQLInvoker(cache, schemaRepository.Object, assetRepository.Object, contentRepository.Object, new FakeUrlGenerator());
}
[Fact]
@ -97,6 +97,8 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy
lastModified
lastModifiedBy
url
thumbnailUrl
mimeType
fileName
fileSize
@ -131,6 +133,8 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy = "subject:user1",
lastModified = assetEntity.LastModified.ToDateTimeUtc(),
lastModifiedBy = "subject:user2",
url = $"assets/{assetEntity.Id}",
thumbnailUrl = $"assets/{assetEntity.Id}?width=100",
mimeType = "image/png",
fileName = "MyFile.png",
fileSize = 1024,
@ -163,6 +167,8 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy
lastModified
lastModifiedBy
url
thumbnailUrl
mimeType
fileName
fileSize
@ -191,6 +197,8 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy = "subject:user1",
lastModified = assetEntity.LastModified.ToDateTimeUtc(),
lastModifiedBy = "subject:user2",
url = $"assets/{assetEntity.Id}",
thumbnailUrl = $"assets/{assetEntity.Id}?width=100",
mimeType = "image/png",
fileName = "MyFile.png",
fileSize = 1024,
@ -219,6 +227,7 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy
lastModified
lastModifiedBy
url
data {
myString {
iv
@ -266,6 +275,7 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy = "subject:user1",
lastModified = contentEntity.LastModified.ToDateTimeUtc(),
lastModifiedBy = "subject:user2",
url = $"contents/my-schema/{contentEntity.Id}",
data = new
{
myString = new
@ -325,6 +335,7 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy
lastModified
lastModifiedBy
url
data {{
myString {{
iv
@ -366,6 +377,7 @@ namespace Squidex.Domain.Apps.Read.Contents
createdBy = "subject:user1",
lastModified = contentEntity.LastModified.ToDateTimeUtc(),
lastModifiedBy = "subject:user2",
url = $"contents/my-schema/{contentEntity.Id}",
data = new
{
myString = new

33
tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs

@ -0,0 +1,33 @@
// ==========================================================================
// FakeUrlGenerator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.Schemas;
namespace Squidex.Domain.Apps.Read.Contents.TestData
{
public sealed class FakeUrlGenerator : IGraphQLUrlGenerator
{
public string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity)
{
return $"assets/{assetEntity.Id}";
}
public string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity)
{
return $"assets/{assetEntity.Id}?width=100";
}
public string GenerateContentUrl(IAppEntity appEntity, ISchemaEntity schemaEntity, IContentEntity contentEntity)
{
return $"contents/{schemaEntity.Name}/{contentEntity.Id}";
}
}
}
Loading…
Cancel
Save