mirror of https://github.com/Squidex/squidex.git
Browse Source
# Conflicts: # backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs # backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs # backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cspull/590/head
96 changed files with 1688 additions and 2298 deletions
@ -1,7 +1,8 @@ |
|||||
cd translator\Squidex.Translator |
cd translator\Squidex.Translator |
||||
|
|
||||
dotnet run translate check-backend D:\Squidex |
dotnet run translate check-backend ..\..\..\.. |
||||
dotnet run translate check-frontend D:\Squidex |
dotnet run translate check-frontend ..\..\..\.. |
||||
|
|
||||
|
dotnet run translate gen-frontend ..\..\..\.. |
||||
|
dotnet run translate gen-backend ..\..\..\.. |
||||
|
|
||||
dotnet run translate gen-frontend D:\Squidex |
|
||||
dotnet run translate gen-backend D:\Squidex |
|
||||
@ -1,10 +1,8 @@ |
|||||
#!/bin/bash |
#!/bin/bash |
||||
PATH=${1:-/Squidex} |
|
||||
|
|
||||
cd translator/Squidex.Translator |
cd translator/Squidex.Translator |
||||
|
|
||||
/usr/local/share/dotnet/dotnet run translate check-backend $1 |
/usr/local/share/dotnet/dotnet run translate check-backend ../../../.. |
||||
/usr/local/share/dotnet/dotnet run translate check-frontend $1 |
/usr/local/share/dotnet/dotnet run translate check-frontend ../../../.. |
||||
|
|
||||
/usr/local/share/dotnet/dotnet run translate gen-frontend $1 |
/usr/local/share/dotnet/dotnet run translate gen-frontend ../../../.. |
||||
/usr/local/share/dotnet/dotnet run translate gen-backend $1 |
/usr/local/share/dotnet/dotnet run translate gen-backend ../../../.. |
||||
@ -0,0 +1,153 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.HandleRules |
||||
|
{ |
||||
|
public class EventEnricherTests |
||||
|
{ |
||||
|
private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); |
||||
|
private readonly EventEnricher sut; |
||||
|
|
||||
|
public EventEnricherTests() |
||||
|
{ |
||||
|
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
||||
|
|
||||
|
sut = new EventEnricher(cache, userResolver); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_enrich_with_timestamp() |
||||
|
{ |
||||
|
var timestamp = SystemClock.Instance.GetCurrentInstant().WithoutMs(); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create<AppEvent>(new ContentCreated()) |
||||
|
.SetTimestamp(timestamp); |
||||
|
|
||||
|
var enrichedEvent = new EnrichedContentEvent(); |
||||
|
|
||||
|
await sut.EnrichAsync(enrichedEvent, @event); |
||||
|
|
||||
|
Assert.Equal(timestamp, enrichedEvent.Timestamp); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_enrich_with_appId() |
||||
|
{ |
||||
|
var appId = NamedId.Of(DomainId.NewGuid(), "my-app"); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create<AppEvent>(new ContentCreated |
||||
|
{ |
||||
|
AppId = appId |
||||
|
}); |
||||
|
|
||||
|
var enrichedEvent = new EnrichedContentEvent(); |
||||
|
|
||||
|
await sut.EnrichAsync(enrichedEvent, @event); |
||||
|
|
||||
|
Assert.Equal(appId, enrichedEvent.AppId); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_enrich_with_user_if_token_is_null() |
||||
|
{ |
||||
|
RefToken actor = null!; |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create<AppEvent>(new ContentCreated |
||||
|
{ |
||||
|
Actor = actor |
||||
|
}); |
||||
|
|
||||
|
var enrichedEvent = new EnrichedContentEvent(); |
||||
|
|
||||
|
await sut.EnrichAsync(enrichedEvent, @event); |
||||
|
|
||||
|
Assert.Null(enrichedEvent.User); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdAsync(A<string>._)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_enrich_with_user() |
||||
|
{ |
||||
|
var actor = new RefToken(RefTokenType.Client, "me"); |
||||
|
|
||||
|
var user = A.Dummy<IUser>(); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier)) |
||||
|
.Returns(user); |
||||
|
|
||||
|
var @event = |
||||
|
Envelope.Create<AppEvent>(new ContentCreated |
||||
|
{ |
||||
|
Actor = actor |
||||
|
}); |
||||
|
|
||||
|
var enrichedEvent = new EnrichedContentEvent(); |
||||
|
|
||||
|
await sut.EnrichAsync(enrichedEvent, @event); |
||||
|
|
||||
|
Assert.Equal(user, enrichedEvent.User); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdAsync(A<string>._)) |
||||
|
.MustHaveHappenedOnceExactly(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_enrich_with_user_and_cache() |
||||
|
{ |
||||
|
var actor = new RefToken(RefTokenType.Client, "me"); |
||||
|
|
||||
|
var user = A.Dummy<IUser>(); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier)) |
||||
|
.Returns(user); |
||||
|
|
||||
|
var @event1 = |
||||
|
Envelope.Create<AppEvent>(new ContentCreated |
||||
|
{ |
||||
|
Actor = actor |
||||
|
}); |
||||
|
|
||||
|
var @event2 = |
||||
|
Envelope.Create<AppEvent>(new ContentCreated |
||||
|
{ |
||||
|
Actor = actor |
||||
|
}); |
||||
|
|
||||
|
var enrichedEvent1 = new EnrichedContentEvent(); |
||||
|
var enrichedEvent2 = new EnrichedContentEvent(); |
||||
|
|
||||
|
await sut.EnrichAsync(enrichedEvent1, @event1); |
||||
|
await sut.EnrichAsync(enrichedEvent2, @event2); |
||||
|
|
||||
|
Assert.Equal(user, enrichedEvent1.User); |
||||
|
Assert.Equal(user, enrichedEvent2.User); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdAsync(A<string>._)) |
||||
|
.MustHaveHappenedOnceExactly(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,191 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; |
||||
|
import { LocalizerService } from './../../services/localizer.service'; |
||||
|
import { formatError } from './error-formatting'; |
||||
|
import { ValidatorsEx } from './validators'; |
||||
|
|
||||
|
describe('formatErrors', () => { |
||||
|
const localizer = new LocalizerService({ |
||||
|
'users.passwordConfirmValidationMessage': 'Passwords must be the same.', |
||||
|
'validation.between': '{field} must be between \'{min}\' and \'{max}\'.', |
||||
|
'validation.betweenlength': '{field|upper} must have between {minlength} and {maxlength} item(s).', |
||||
|
'validation.betweenlengthstring': '{field|upper} must have between {minlength} and {maxlength} character(s).', |
||||
|
'validation.email': '{field|upper} must be an email address.', |
||||
|
'validation.exactly': '{field|upper} must be exactly \'{expected}\'.', |
||||
|
'validation.exactlylength': '{field|upper} must have exactly {expected} item(s).', |
||||
|
'validation.exactlylengthstring': '{field|upper} must have exactly {expected} character(s).', |
||||
|
'validation.match': '{message}', |
||||
|
'validation.max': '{field|upper} must be less or equal to \'{max}\'.', |
||||
|
'validation.maxlength': '{field|upper} must not have more than {requiredlength} item(s).', |
||||
|
'validation.maxlengthstring': '{field|upper} must not have more than {requiredlength} character(s).', |
||||
|
'validation.min': '{field|upper} must be greater or equal to \'{min}\'.', |
||||
|
'validation.minlength': '{field|upper} must have at least {requiredlength} item(s).', |
||||
|
'validation.minlengthstring': '{field|upper} must have at least {requiredlength} character(s).', |
||||
|
'validation.pattern': '{field|upper} does not match to the pattern.', |
||||
|
'validation.patternmessage': '{message}', |
||||
|
'validation.required': '{field|upper} is required.', |
||||
|
'validation.requiredTrue': '{field|upper} is required.', |
||||
|
'validation.uniquestrings': '{field|upper} must not contain duplicate values.', |
||||
|
'validation.validarrayvalues': '{field|upper} contains an invalid value: {invalidvalue}.', |
||||
|
'validation.validdatetime': '{field|upper} is not a valid date time.', |
||||
|
'validation.validvalues': '{field|upper} is not a valid value.' |
||||
|
}); |
||||
|
|
||||
|
it('should format min', () => { |
||||
|
const error = validate(1, Validators.min(2)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must be greater or equal to \'2\'.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format max', () => { |
||||
|
const error = validate(3, Validators.max(2)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must be less or equal to \'2\'.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format required', () => { |
||||
|
const error = validate(undefined, Validators.required); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD is required.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format requiredTrue', () => { |
||||
|
const error = validate(undefined, Validators.requiredTrue); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD is required.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format email', () => { |
||||
|
const error = validate('invalid', Validators.email); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must be an email address.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format minLength string', () => { |
||||
|
const error = validate('x', Validators.minLength(2)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must have at least 2 character(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format maxLength string', () => { |
||||
|
const error = validate('xxx', Validators.maxLength(2)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must not have more than 2 character(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format minLength array', () => { |
||||
|
const error = validate([1], Validators.minLength(2)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must have at least 2 item(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format maxLength array', () => { |
||||
|
const error = validate([1, 1, 1], Validators.maxLength(2)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must not have more than 2 item(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format match', () => { |
||||
|
const error = validate('123', Validators.pattern('[A-Z]')); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD does not match to the pattern.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format match with message', () => { |
||||
|
const error = validate('123', ValidatorsEx.pattern('[A-Z]', 'Custom Message')); |
||||
|
|
||||
|
expect(error).toEqual('Custom Message'); |
||||
|
}); |
||||
|
|
||||
|
it('should format between exactly', () => { |
||||
|
const error = validate(2, ValidatorsEx.between(3, 3)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must be exactly \'3\'.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format between range', () => { |
||||
|
const error = validate(2, ValidatorsEx.between(3, 5)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must be between \'3\' and \'5\'.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format betweenLength string exactly', () => { |
||||
|
const error = validate('xx', ValidatorsEx.betweenLength(3, 3)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must have exactly 3 character(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format betweenLength string range', () => { |
||||
|
const error = validate('xx', ValidatorsEx.betweenLength(3, 5)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must have between 3 and 5 character(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format betweenLength array exactly', () => { |
||||
|
const error = validate([1], ValidatorsEx.betweenLength(3, 3)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must have exactly 3 item(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format betweenLength array range', () => { |
||||
|
const error = validate([1, 1], ValidatorsEx.betweenLength(3, 5)); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must have between 3 and 5 item(s).'); |
||||
|
}); |
||||
|
|
||||
|
it('should format validDateTime', () => { |
||||
|
const error = validate('invalid', ValidatorsEx.validDateTime()); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD is not a valid date time.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format validValues', () => { |
||||
|
const error = validate(5, ValidatorsEx.validValues([1, 2, 3])); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD is not a valid value.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format validArrayValues', () => { |
||||
|
const error = validate([2, 4], ValidatorsEx.validArrayValues([1, 2, 3])); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD contains an invalid value: 4.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format uniqueStrings', () => { |
||||
|
const error = validate(['1', '2', '2', '3'], ValidatorsEx.uniqueStrings()); |
||||
|
|
||||
|
expect(error).toEqual('MY_FIELD must not contain duplicate values.'); |
||||
|
}); |
||||
|
|
||||
|
it('should format match', () => { |
||||
|
const formControl1 = new FormControl(1); |
||||
|
const formControl2 = new FormControl(2); |
||||
|
|
||||
|
const formGroup = new FormGroup({ |
||||
|
field1: formControl1, |
||||
|
field2: formControl2 |
||||
|
}); |
||||
|
|
||||
|
const formError = ValidatorsEx.match('field2', 'i18n:users.passwordConfirmValidationMessage')!(formControl1)!; |
||||
|
const formMessage = formatError(localizer, 'MY_FIELD', Object.keys(formError)[0], Object.values(formError)[0], undefined); |
||||
|
|
||||
|
expect(formMessage).toEqual('Passwords must be the same.'); |
||||
|
|
||||
|
formGroup.reset(); |
||||
|
}); |
||||
|
|
||||
|
function validate(value: any, validator: ValidatorFn) { |
||||
|
const formControl = new FormControl(value); |
||||
|
|
||||
|
const formError = validator(formControl)!; |
||||
|
const formMessage = formatError(localizer, 'MY_FIELD', Object.keys(formError)[0], Object.values(formError)[0], value); |
||||
|
|
||||
|
return formMessage; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,47 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights r vbeserved |
||||
|
*/ |
||||
|
|
||||
|
import { Types } from '@app/framework/internal'; |
||||
|
import { LocalizerService } from '@app/shared'; |
||||
|
|
||||
|
export function formatError(localizer: LocalizerService, field: string, type: string, properties: any, value: any, errors?: any) { |
||||
|
type = type.toLowerCase(); |
||||
|
|
||||
|
if (Types.isString(value)) { |
||||
|
if (type === 'minlength') { |
||||
|
type = 'minlengthstring'; |
||||
|
} |
||||
|
|
||||
|
if (type === 'maxlength') { |
||||
|
type = 'maxlengthstring'; |
||||
|
} |
||||
|
|
||||
|
if (type === 'exactlylength') { |
||||
|
type = 'exactlylengthstring'; |
||||
|
} |
||||
|
|
||||
|
if (type === 'betweenlength') { |
||||
|
type = 'betweenlengthstring'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let message: string | null = properties['message']; |
||||
|
|
||||
|
if (!Types.isString(message) && errors) { |
||||
|
message = errors[type]; |
||||
|
} |
||||
|
|
||||
|
if (!Types.isString(message)) { |
||||
|
message = `validation.${type}`; |
||||
|
} |
||||
|
|
||||
|
const args = { ...properties, field }; |
||||
|
|
||||
|
message = localizer.getOrKey(message, args); |
||||
|
|
||||
|
return message; |
||||
|
} |
||||
@ -1,9 +1,10 @@ |
|||||
<ng-container *ngIf="show"> |
<ng-container *ngIf="show"> |
||||
<div [class.form-bubble]="bubble"> |
<div [class.form-bubble]="bubble"> |
||||
<div class="form-alert form-alert-error" [class.closeable]="closeable"> |
<div class="form-alert form-alert-error" [class.closeable]="closeable"> |
||||
<a class="form-alert-close" (click)="close()"> |
<span class="form-alert-close" (click)="close()"> |
||||
<i class="icon-close"></i> |
<i class="icon-close"></i> |
||||
</a> |
</span> |
||||
|
|
||||
<div [innerHTML]="error | sqxTranslate | sqxMarkdown"></div> |
<div [innerHTML]="error | sqxTranslate | sqxMarkdown"></div> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
|
|||||
File diff suppressed because it is too large
@ -1,344 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using GraphQL; |
|
||||
using GraphQL.Resolvers; |
|
||||
using GraphQL.Types; |
|
||||
using Newtonsoft.Json.Linq; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|
||||
{ |
|
||||
public sealed class AppMutationsGraphType : ObjectGraphType |
|
||||
{ |
|
||||
public AppMutationsGraphType(IGraphModel model, IEnumerable<ISchemaEntity> schemas) |
|
||||
{ |
|
||||
foreach (var schema in schemas) |
|
||||
{ |
|
||||
var schemaId = schema.NamedId(); |
|
||||
var schemaType = schema.TypeName(); |
|
||||
var schemaName = schema.DisplayName(); |
|
||||
|
|
||||
var contentType = model.GetContentType(schema.Id); |
|
||||
var contentDataType = model.GetContentDataType(schema.Id); |
|
||||
|
|
||||
var resultType = new ContentDataChangedResultGraphType(schemaType, schemaName, contentDataType); |
|
||||
|
|
||||
var inputType = new ContentDataGraphInputType(model, schema); |
|
||||
|
|
||||
AddContentCreate(schemaId, schemaType, schemaName, inputType, contentDataType, contentType); |
|
||||
AddContentUpdate(schemaType, schemaName, inputType, resultType); |
|
||||
AddContentPatch(schemaType, schemaName, inputType, resultType); |
|
||||
AddContentPublish(schemaType, schemaName); |
|
||||
AddContentUnpublish(schemaType, schemaName); |
|
||||
AddContentArchive(schemaType, schemaName); |
|
||||
AddContentRestore(schemaType, schemaName); |
|
||||
AddContentDelete(schemaType, schemaName); |
|
||||
} |
|
||||
|
|
||||
Description = "The app mutations."; |
|
||||
} |
|
||||
|
|
||||
private void AddContentCreate(NamedId<Guid> schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType contentDataType, IGraphType contentType) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"create{schemaType}Content", |
|
||||
Arguments = new QueryArguments |
|
||||
{ |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "data", |
|
||||
Description = $"The data for the {schemaName} content.", |
|
||||
DefaultValue = null, |
|
||||
ResolvedType = new NonNullGraphType(inputType), |
|
||||
}, |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "publish", |
|
||||
Description = "Set to true to autopublish content.", |
|
||||
DefaultValue = false, |
|
||||
ResolvedType = AllTypes.Boolean |
|
||||
}, |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "expectedVersion", |
|
||||
Description = "The expected version", |
|
||||
DefaultValue = EtagVersion.Any, |
|
||||
ResolvedType = AllTypes.Int |
|
||||
} |
|
||||
}, |
|
||||
ResolvedType = new NonNullGraphType(contentType), |
|
||||
Resolver = ResolveAsync(async (c, publish) => |
|
||||
{ |
|
||||
var argPublish = c.GetArgument<bool>("publish"); |
|
||||
|
|
||||
var contentData = GetContentData(c); |
|
||||
|
|
||||
var command = new CreateContent { SchemaId = schemaId, Data = contentData, Publish = argPublish }; |
|
||||
var commandContext = await publish(command); |
|
||||
|
|
||||
var result = commandContext.Result<EntityCreatedResult<NamedContentData>>(); |
|
||||
var response = ContentEntity.Create(command, result); |
|
||||
|
|
||||
return (IContentEntity)ContentEntity.Create(command, result); |
|
||||
}), |
|
||||
Description = $"Creates an {schemaName} content." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void AddContentUpdate(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"update{schemaType}Content", |
|
||||
Arguments = new QueryArguments |
|
||||
{ |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "id", |
|
||||
Description = $"The id of the {schemaName} content (GUID)", |
|
||||
DefaultValue = string.Empty, |
|
||||
ResolvedType = AllTypes.NonNullGuid |
|
||||
}, |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "data", |
|
||||
Description = $"The data for the {schemaName} content.", |
|
||||
DefaultValue = null, |
|
||||
ResolvedType = new NonNullGraphType(inputType), |
|
||||
}, |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "expectedVersion", |
|
||||
Description = "The expected version", |
|
||||
DefaultValue = EtagVersion.Any, |
|
||||
ResolvedType = AllTypes.Int |
|
||||
} |
|
||||
}, |
|
||||
ResolvedType = new NonNullGraphType(resultType), |
|
||||
Resolver = ResolveAsync(async (c, publish) => |
|
||||
{ |
|
||||
var contentId = c.GetArgument<Guid>("id"); |
|
||||
var contentData = GetContentData(c); |
|
||||
|
|
||||
var command = new UpdateContent { ContentId = contentId, Data = contentData }; |
|
||||
var commandContext = await publish(command); |
|
||||
|
|
||||
var result = commandContext.Result<ContentDataChangedResult>(); |
|
||||
|
|
||||
return result; |
|
||||
}), |
|
||||
Description = $"Update an {schemaName} content by id." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void AddContentPatch(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"patch{schemaType}Content", |
|
||||
Arguments = new QueryArguments |
|
||||
{ |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "id", |
|
||||
Description = $"The id of the {schemaName} content (GUID)", |
|
||||
DefaultValue = string.Empty, |
|
||||
ResolvedType = AllTypes.NonNullGuid |
|
||||
}, |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "data", |
|
||||
Description = $"The data for the {schemaName} content.", |
|
||||
DefaultValue = null, |
|
||||
ResolvedType = new NonNullGraphType(inputType), |
|
||||
}, |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "expectedVersion", |
|
||||
Description = "The expected version", |
|
||||
DefaultValue = EtagVersion.Any, |
|
||||
ResolvedType = AllTypes.Int |
|
||||
} |
|
||||
}, |
|
||||
ResolvedType = new NonNullGraphType(resultType), |
|
||||
Resolver = ResolveAsync(async (c, publish) => |
|
||||
{ |
|
||||
var contentId = c.GetArgument<Guid>("id"); |
|
||||
var contentData = GetContentData(c); |
|
||||
|
|
||||
var command = new PatchContent { ContentId = contentId, Data = contentData }; |
|
||||
var commandContext = await publish(command); |
|
||||
|
|
||||
var result = commandContext.Result<ContentDataChangedResult>(); |
|
||||
|
|
||||
return result; |
|
||||
}), |
|
||||
Description = $"Patch a {schemaName} content." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void AddContentPublish(string schemaType, string schemaName) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"publish{schemaType}Content", |
|
||||
Arguments = CreateIdArguments(schemaName), |
|
||||
ResolvedType = AllTypes.CommandVersion, |
|
||||
Resolver = ResolveAsync((c, publish) => |
|
||||
{ |
|
||||
var contentId = c.GetArgument<Guid>("id"); |
|
||||
|
|
||||
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Published }; |
|
||||
|
|
||||
return publish(command); |
|
||||
}), |
|
||||
Description = $"Publish a {schemaName} content." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void AddContentUnpublish(string schemaType, string schemaName) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"unpublish{schemaType}Content", |
|
||||
Arguments = CreateIdArguments(schemaName), |
|
||||
ResolvedType = AllTypes.CommandVersion, |
|
||||
Resolver = ResolveAsync((c, publish) => |
|
||||
{ |
|
||||
var contentId = c.GetArgument<Guid>("id"); |
|
||||
|
|
||||
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft }; |
|
||||
|
|
||||
return publish(command); |
|
||||
}), |
|
||||
Description = $"Unpublish a {schemaName} content." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void AddContentArchive(string schemaType, string schemaName) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"archive{schemaType}Content", |
|
||||
Arguments = CreateIdArguments(schemaName), |
|
||||
ResolvedType = AllTypes.CommandVersion, |
|
||||
Resolver = ResolveAsync((c, publish) => |
|
||||
{ |
|
||||
var contentId = c.GetArgument<Guid>("id"); |
|
||||
|
|
||||
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Archived }; |
|
||||
|
|
||||
return publish(command); |
|
||||
}), |
|
||||
Description = $"Archive a {schemaName} content." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void AddContentRestore(string schemaType, string schemaName) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"restore{schemaType}Content", |
|
||||
Arguments = CreateIdArguments(schemaName), |
|
||||
ResolvedType = AllTypes.CommandVersion, |
|
||||
Resolver = ResolveAsync((c, publish) => |
|
||||
{ |
|
||||
var contentId = c.GetArgument<Guid>("id"); |
|
||||
|
|
||||
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft }; |
|
||||
|
|
||||
return publish(command); |
|
||||
}), |
|
||||
Description = $"Restore a {schemaName} content." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void AddContentDelete(string schemaType, string schemaName) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = $"delete{schemaType}Content", |
|
||||
Arguments = CreateIdArguments(schemaName), |
|
||||
ResolvedType = AllTypes.CommandVersion, |
|
||||
Resolver = ResolveAsync((c, publish) => |
|
||||
{ |
|
||||
var contentId = c.GetArgument<Guid>("id"); |
|
||||
|
|
||||
var command = new DeleteContent { ContentId = contentId }; |
|
||||
|
|
||||
return publish(command); |
|
||||
}), |
|
||||
Description = $"Delete an {schemaName} content." |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private static QueryArguments CreateIdArguments(string schemaName) |
|
||||
{ |
|
||||
return new QueryArguments |
|
||||
{ |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "id", |
|
||||
Description = $"The id of the {schemaName} content (GUID)", |
|
||||
DefaultValue = string.Empty, |
|
||||
ResolvedType = AllTypes.NonNullGuid |
|
||||
}, |
|
||||
new QueryArgument(AllTypes.None) |
|
||||
{ |
|
||||
Name = "expectedVersion", |
|
||||
Description = "The expected version", |
|
||||
DefaultValue = EtagVersion.Any, |
|
||||
ResolvedType = AllTypes.Int |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
private static IFieldResolver ResolveAsync<T>(Func<ResolveFieldContext, Func<SquidexCommand, Task<CommandContext>>, Task<T>> action) |
|
||||
{ |
|
||||
return new FuncFieldResolver<Task<T>>(async c => |
|
||||
{ |
|
||||
var e = (GraphQLExecutionContext)c.UserContext; |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
return await action(c, command => |
|
||||
{ |
|
||||
command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any); |
|
||||
|
|
||||
return e.CommandBus.PublishAsync(command); |
|
||||
}); |
|
||||
} |
|
||||
catch (ValidationException ex) |
|
||||
{ |
|
||||
c.Errors.Add(new ExecutionError(ex.Message)); |
|
||||
|
|
||||
throw; |
|
||||
} |
|
||||
catch (DomainException ex) |
|
||||
{ |
|
||||
c.Errors.Add(new ExecutionError(ex.Message)); |
|
||||
|
|
||||
throw; |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private static NamedContentData GetContentData(ResolveFieldContext c) |
|
||||
{ |
|
||||
return JObject.FromObject(c.GetArgument<object>("data")).ToObject<NamedContentData>(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,70 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Linq; |
|
||||
using GraphQL.Types; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|
||||
{ |
|
||||
public sealed class ContentDataGraphInputType : InputObjectGraphType |
|
||||
{ |
|
||||
public ContentDataGraphInputType(IGraphModel model, ISchemaEntity schema) |
|
||||
{ |
|
||||
var schemaType = schema.TypeName(); |
|
||||
var schemaName = schema.DisplayName(); |
|
||||
|
|
||||
Name = $"{schemaType}InputDto"; |
|
||||
|
|
||||
foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden)) |
|
||||
{ |
|
||||
var inputType = model.GetInputGraphType(field); |
|
||||
|
|
||||
if (inputType != null) |
|
||||
{ |
|
||||
if (field.RawProperties.IsRequired) |
|
||||
{ |
|
||||
inputType = new NonNullGraphType(inputType); |
|
||||
} |
|
||||
|
|
||||
var fieldName = field.RawProperties.Label.WithFallback(field.Name); |
|
||||
|
|
||||
var fieldGraphType = new InputObjectGraphType |
|
||||
{ |
|
||||
Name = $"{schemaType}Data{field.Name.ToPascalCase()}InputDto" |
|
||||
}; |
|
||||
|
|
||||
var partition = model.ResolvePartition(field.Partitioning); |
|
||||
|
|
||||
foreach (var partitionItem in partition) |
|
||||
{ |
|
||||
fieldGraphType.AddField(new FieldType |
|
||||
{ |
|
||||
Name = partitionItem.Key, |
|
||||
Resolver = null, |
|
||||
ResolvedType = inputType, |
|
||||
Description = field.RawProperties.Hints |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
fieldGraphType.Description = $"The input structure of the {fieldName} of a {schemaName} content type."; |
|
||||
|
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = field.Name.ToCamelCase(), |
|
||||
Resolver = null, |
|
||||
ResolvedType = fieldGraphType, |
|
||||
Description = $"The {fieldName} field." |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Description = $"The structure of a {schemaName} content type."; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,31 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// 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 |
|
||||
{ |
|
||||
public sealed class GeolocationInputGraphType : InputObjectGraphType |
|
||||
{ |
|
||||
public GeolocationInputGraphType() |
|
||||
{ |
|
||||
Name = "GeolocationInputDto"; |
|
||||
|
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = "latitude", |
|
||||
ResolvedType = AllTypes.NonNullFloat |
|
||||
}); |
|
||||
|
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = "longitude", |
|
||||
ResolvedType = AllTypes.NonNullFloat |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using GraphQL.Types; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|
||||
{ |
|
||||
public static class InputFieldExtensions |
|
||||
{ |
|
||||
public static IGraphType GetInputGraphType(this IField field) |
|
||||
{ |
|
||||
return field.Accept(InputFieldVisitor.Default); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,71 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using GraphQL.Types; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|
||||
{ |
|
||||
public sealed class InputFieldVisitor : IFieldVisitor<IGraphType> |
|
||||
{ |
|
||||
public static readonly InputFieldVisitor Default = new InputFieldVisitor(); |
|
||||
|
|
||||
private InputFieldVisitor() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IArrayField field) |
|
||||
{ |
|
||||
return AllTypes.NoopArray; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<AssetsFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.References; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<BooleanFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.Boolean; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<DateTimeFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.Date; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<GeolocationFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.GeolocationInput; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<JsonFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.Json; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<NumberFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.Float; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<ReferencesFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.References; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<StringFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.String; |
|
||||
} |
|
||||
|
|
||||
public IGraphType Visit(IField<TagsFieldProperties> field) |
|
||||
{ |
|
||||
return AllTypes.Tags; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Linq; |
|
||||
using GraphQL.Types; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|
||||
{ |
|
||||
public sealed class NestedInputGraphType : InputObjectGraphType |
|
||||
{ |
|
||||
public NestedInputGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field) |
|
||||
{ |
|
||||
var schemaType = schema.TypeName(); |
|
||||
var schemaName = schema.DisplayName(); |
|
||||
|
|
||||
var fieldType = field.TypeName(); |
|
||||
var fieldName = field.DisplayName(); |
|
||||
|
|
||||
Name = $"{schemaType}{fieldName}ChildDto"; |
|
||||
|
|
||||
foreach (var nestedField in field.Fields.Where(x => !x.IsHidden)) |
|
||||
{ |
|
||||
var fieldInfo = model.GetGraphType(schema, nestedField); |
|
||||
|
|
||||
if (fieldInfo.ResolveType != null) |
|
||||
{ |
|
||||
AddField(new FieldType |
|
||||
{ |
|
||||
Name = nestedField.Name.ToCamelCase(), |
|
||||
Resolver = null, |
|
||||
ResolvedType = fieldInfo.ResolveType, |
|
||||
Description = $"The {fieldName}/{nestedField.DisplayName()} nested field." |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Description = $"The structure of a {schemaName}.{fieldName} nested schema."; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue