mirror of https://github.com/Squidex/squidex.git
30 changed files with 741 additions and 211 deletions
@ -0,0 +1,75 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL; |
|||
using GraphQL.Resolvers; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
|
|||
#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Dynamic |
|||
{ |
|||
internal sealed class DynamicResolver : IFieldResolver |
|||
{ |
|||
public static readonly DynamicResolver Instance = new DynamicResolver(); |
|||
|
|||
public async ValueTask<object?> ResolveAsync(IResolveFieldContext context) |
|||
{ |
|||
if (context.Source is JsonObject jsonObject) |
|||
{ |
|||
var name = context.FieldDefinition.Name; |
|||
|
|||
if (!jsonObject.TryGetValue(name, out var jsonValue)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var value = Convert(jsonValue); |
|||
|
|||
return value; |
|||
} |
|||
|
|||
var result = await NameFieldResolver.Instance.ResolveAsync(context); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static object? Convert(JsonValue json) |
|||
{ |
|||
var value = json.Value; |
|||
|
|||
switch (value) |
|||
{ |
|||
case double d: |
|||
{ |
|||
var asInteger = (long)d; |
|||
|
|||
if (asInteger == d) |
|||
{ |
|||
return asInteger; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
case JsonArray a: |
|||
{ |
|||
var result = new List<object?>(); |
|||
|
|||
foreach (var item in a) |
|||
{ |
|||
result.Add(Convert(item)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL.Types; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Dynamic |
|||
{ |
|||
internal static class DynamicSchemaBuilder |
|||
{ |
|||
public static IGraphType[] ParseTypes(string? typeDefinitions, ReservedNames typeNames) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(typeDefinitions)) |
|||
{ |
|||
return Array.Empty<IGraphType>(); |
|||
} |
|||
|
|||
Schema schema; |
|||
try |
|||
{ |
|||
schema = Schema.For(typeDefinitions); |
|||
} |
|||
catch |
|||
{ |
|||
return Array.Empty<IGraphType>(); |
|||
} |
|||
|
|||
var map = schema.AdditionalTypeInstances.ToDictionary(x => x.Name); |
|||
|
|||
IGraphType? Convert(IGraphType? type) |
|||
{ |
|||
switch (type) |
|||
{ |
|||
case GraphQLTypeReference reference: |
|||
return map.GetValueOrDefault(reference.TypeName) ?? reference; |
|||
case NonNullGraphType nonNull: |
|||
return new NonNullGraphType(Convert(nonNull.ResolvedType)); |
|||
case ListGraphType list: |
|||
return new ListGraphType(Convert(list.ResolvedType)); |
|||
default: |
|||
return type; |
|||
} |
|||
} |
|||
|
|||
var result = new List<IGraphType>(); |
|||
|
|||
foreach (var type in schema.AdditionalTypeInstances) |
|||
{ |
|||
if (type is IComplexGraphType complexGraphType) |
|||
{ |
|||
type.Name = typeNames[type.Name]; |
|||
|
|||
foreach (var field in complexGraphType.Fields) |
|||
{ |
|||
// Assign a resolver to support json values.
|
|||
field.Resolver = DynamicResolver.Instance; |
|||
field.ResolvedType = Convert(field.ResolvedType); |
|||
} |
|||
} |
|||
|
|||
result.Add(type); |
|||
} |
|||
|
|||
return result.ToArray(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using GraphQL.Utilities; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|||
{ |
|||
internal sealed class ErrorVisitor : BaseSchemaNodeVisitor |
|||
{ |
|||
public static readonly ErrorVisitor Instance = new ErrorVisitor(); |
|||
|
|||
internal sealed class ErrorResolver : IFieldResolver |
|||
{ |
|||
private readonly IFieldResolver inner; |
|||
|
|||
public ErrorResolver(IFieldResolver inner) |
|||
{ |
|||
this.inner = inner; |
|||
} |
|||
|
|||
public async ValueTask<object?> ResolveAsync(IResolveFieldContext context) |
|||
{ |
|||
try |
|||
{ |
|||
return await inner.ResolveAsync(context); |
|||
} |
|||
catch (ValidationException ex) |
|||
{ |
|||
throw new ExecutionError(ex.Message); |
|||
} |
|||
catch (DomainException ex) |
|||
{ |
|||
throw new ExecutionError(ex.Message); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>(); |
|||
|
|||
logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name); |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal sealed class ErrorSourceStreamResolver : ISourceStreamResolver |
|||
{ |
|||
private readonly ISourceStreamResolver inner; |
|||
|
|||
public ErrorSourceStreamResolver(ISourceStreamResolver inner) |
|||
{ |
|||
this.inner = inner; |
|||
} |
|||
|
|||
public async ValueTask<IObservable<object?>> ResolveAsync(IResolveFieldContext context) |
|||
{ |
|||
try |
|||
{ |
|||
return await inner.ResolveAsync(context); |
|||
} |
|||
catch (ValidationException ex) |
|||
{ |
|||
throw new ExecutionError(ex.Message); |
|||
} |
|||
catch (DomainException ex) |
|||
{ |
|||
throw new ExecutionError(ex.Message); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>(); |
|||
|
|||
logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name); |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private ErrorVisitor() |
|||
{ |
|||
} |
|||
|
|||
public override void VisitObjectFieldDefinition(FieldType field, IObjectGraphType type, ISchema schema) |
|||
{ |
|||
if (type.Name.StartsWith("__", StringComparison.Ordinal)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (field.StreamResolver != null) |
|||
{ |
|||
if (field.StreamResolver is ErrorSourceStreamResolver) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
field.StreamResolver = new ErrorSourceStreamResolver(field.StreamResolver); |
|||
} |
|||
else |
|||
{ |
|||
if (field.Resolver is ErrorResolver) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
field.Resolver = new ErrorResolver(field.Resolver ?? NameFieldResolver.Instance); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class ReservedNames |
|||
{ |
|||
private readonly Dictionary<string, int> takenNames; |
|||
|
|||
public string this[string name] |
|||
{ |
|||
get => GetName(name); |
|||
} |
|||
|
|||
private ReservedNames(Dictionary<string, int> takenNames) |
|||
{ |
|||
this.takenNames = takenNames; |
|||
} |
|||
|
|||
public static ReservedNames ForFields() |
|||
{ |
|||
var reserved = new Dictionary<string, int>(); |
|||
|
|||
return new ReservedNames(reserved); |
|||
} |
|||
|
|||
public static ReservedNames ForTypes() |
|||
{ |
|||
// Reserver names that are used for other GraphQL types.
|
|||
var reserved = new Dictionary<string, int> |
|||
{ |
|||
["Asset"] = 1, |
|||
["AssetResultDto"] = 1, |
|||
["Content"] = 1, |
|||
["Component"] = 1, |
|||
["EnrichedAssetEvent"] = 1, |
|||
["EnrichedContentEvent"] = 1, |
|||
["EntityCreatedResultDto"] = 1, |
|||
["EntitySavedResultDto"] = 1, |
|||
["JsonObject"] = 1, |
|||
["JsonScalar"] = 1, |
|||
["JsonPrimitive"] = 1, |
|||
["User"] = 1, |
|||
}; |
|||
|
|||
return new ReservedNames(reserved); |
|||
} |
|||
|
|||
private string GetName(string name) |
|||
{ |
|||
Guard.NotNullOrEmpty(name); |
|||
|
|||
if (!char.IsLetter(name[0])) |
|||
{ |
|||
name = "gql_" + name; |
|||
} |
|||
|
|||
if (!takenNames.TryGetValue(name, out var offset)) |
|||
{ |
|||
// If the name is free, we do not add an offset.
|
|||
takenNames[name] = 1; |
|||
|
|||
return name; |
|||
} |
|||
else |
|||
{ |
|||
// Add + 1 to all offsets for backwards-compatibility.
|
|||
takenNames[name] = ++offset; |
|||
|
|||
return $"{name}{offset}"; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public sealed class NamesTests |
|||
{ |
|||
[Fact] |
|||
public void Should_return_name_if_not_taken() |
|||
{ |
|||
var sut = ReservedNames.ForFields(); |
|||
|
|||
var result = sut["myName"]; |
|||
|
|||
Assert.Equal("myName", result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_corrected_name_if_not_taken() |
|||
{ |
|||
var sut = ReservedNames.ForFields(); |
|||
|
|||
var result = sut["2myName"]; |
|||
|
|||
Assert.Equal("gql_2myName", result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_name_with_offset_if_taken() |
|||
{ |
|||
var sut = ReservedNames.ForFields(); |
|||
|
|||
var result1 = sut["myName"]; |
|||
var result2 = sut["myName"]; |
|||
var result3 = sut["myName"]; |
|||
|
|||
Assert.Equal("myName", result1); |
|||
Assert.Equal("myName2", result2); |
|||
Assert.Equal("myName3", result3); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_corrected_name_with_offset_if_taken() |
|||
{ |
|||
var sut = ReservedNames.ForFields(); |
|||
|
|||
var result1 = sut["2myName"]; |
|||
var result2 = sut["2myName"]; |
|||
var result3 = sut["2myName"]; |
|||
|
|||
Assert.Equal("gql_2myName", result1); |
|||
Assert.Equal("gql_2myName2", result2); |
|||
Assert.Equal("gql_2myName3", result3); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_name_with_offset_if_reserved() |
|||
{ |
|||
var sut = ReservedNames.ForTypes(); |
|||
|
|||
var result = sut["Content"]; |
|||
|
|||
Assert.Equal("Content2", result); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<div [formGroup]="fieldForm"> |
|||
<div class="form-group"> |
|||
<label>{{ 'schemas.field.graphQLSchema' | sqxTranslate }}</label> |
|||
|
|||
<sqx-code-editor formControlName="graphQLSchema" mode="ace/mode/graphqlschema" [height]="350"></sqx-code-editor> |
|||
|
|||
<sqx-form-hint> |
|||
{{ 'schemas.field.graphQLSchemaHint' | sqxTranslate }} |
|||
</sqx-form-hint> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,2 @@ |
|||
@import 'mixins'; |
|||
@import 'vars'; |
|||
@ -0,0 +1,26 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, Input } from '@angular/core'; |
|||
import { FormGroup } from '@angular/forms'; |
|||
import { FieldDto, JsonFieldPropertiesDto } from '@app/shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-json-more[field][fieldForm][properties]', |
|||
styleUrls: ['json-more.component.scss'], |
|||
templateUrl: 'json-more.component.html', |
|||
}) |
|||
export class JsonMoreComponent { |
|||
@Input() |
|||
public fieldForm!: FormGroup; |
|||
|
|||
@Input() |
|||
public field!: FieldDto; |
|||
|
|||
@Input() |
|||
public properties!: JsonFieldPropertiesDto; |
|||
} |
|||
Loading…
Reference in new issue