mirror of https://github.com/Squidex/squidex.git
Browse Source
* Improvements to teams. * Fixes * Fixes api * Fixes to teams. * Simplify some interfaces. * Teemp * Fix tests and type names. * Temp * Added UI and a few fixes. * Improve type parsing.pull/925/head
committed by
GitHub
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