From 857af0f0f82b7ffe644bb279517e94c809674e2c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 5 Mar 2017 19:44:31 +0100 Subject: [PATCH] Create api returns new content, populated with default values. --- src/Squidex.Core/Schemas/Field.cs | 7 +++-- .../CQRS/Commands/EntityCreatedResult.cs | 8 +++++ src/Squidex.Write/Apps/AppCommandHandler.cs | 27 +++++++++------- .../Contents/ContentCommandHandler.cs | 24 ++++++++------ .../Schemas/SchemaCommandHandler.cs | 9 ++++-- .../ContentApi/ContentsController.cs | 9 +++--- .../Generator/SchemasSwaggerGenerator.cs | 31 ++++++++++--------- .../ContentApi/Models/ContentDto.cs | 23 +++++++++++++- .../pages/content/content-page.component.ts | 2 +- .../pages/contents/contents-page.component.ts | 3 ++ .../app/framework/angular/http-utils.ts | 17 +++++----- .../shared/services/contents.service.spec.ts | 22 +++++++++---- .../app/shared/services/contents.service.ts | 13 ++++++-- .../Apps/AppCommandHandlerTests.cs | 5 +-- .../Contents/ContentCommandHandlerTests.cs | 3 ++ .../Schemas/SchemaCommandHandlerTests.cs | 3 ++ 16 files changed, 144 insertions(+), 62 deletions(-) diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 02a1ee2a0..3d09dd8b0 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -74,9 +74,12 @@ namespace Squidex.Core.Schemas var defaultValue = RawProperties.GetDefaultValue(); - if (!RawProperties.IsRequired && defaultValue != null && fieldData.GetOrDefault(language.Iso2Code) == null) + if (!RawProperties.IsRequired && defaultValue != null) { - fieldData.AddValue(language.Iso2Code, defaultValue); + if (!fieldData.TryGetValue(language.Iso2Code, out JToken value) || value == null || value.Type == JTokenType.Null) + { + fieldData.AddValue(language.Iso2Code, defaultValue); + } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs index 548d83429..0fbdf5df3 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs @@ -18,4 +18,12 @@ namespace Squidex.Infrastructure.CQRS.Commands IdOrValue = idOrValue; } } + + public static class EntityCreatedResult + { + public static EntityCreatedResult Create(T idOrValue, long version) + { + return new EntityCreatedResult(idOrValue, version); + } + } } diff --git a/src/Squidex.Write/Apps/AppCommandHandler.cs b/src/Squidex.Write/Apps/AppCommandHandler.cs index 99d46fa58..6657528e2 100644 --- a/src/Squidex.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Write/Apps/AppCommandHandler.cs @@ -52,7 +52,12 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot create a new app", error); } - await handler.CreateAsync(context, x => x.Create(command)); + await handler.CreateAsync(context, a => + { + a.Create(command); + + context.Succeed(EntityCreatedResult.Create(a.Id, a.Version)); + }); } protected async Task On(AssignContributor command, CommandContext context) @@ -66,47 +71,47 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot assign contributor to app", error); } - await handler.UpdateAsync(context, x => x.AssignContributor(command)); + await handler.UpdateAsync(context, a => a.AssignContributor(command)); } protected Task On(AttachClient command, CommandContext context) { - return handler.UpdateAsync(context, x => + return handler.UpdateAsync(context, a => { - x.AttachClient(command, keyGenerator.GenerateKey()); + a.AttachClient(command, keyGenerator.GenerateKey()); - context.Succeed(new EntityCreatedResult(x.Clients[command.Id], x.Version)); + context.Succeed(EntityCreatedResult.Create(a.Clients[command.Id], a.Version)); }); } protected Task On(RemoveContributor command, CommandContext context) { - return handler.UpdateAsync(context, x => x.RemoveContributor(command)); + return handler.UpdateAsync(context, a => a.RemoveContributor(command)); } protected Task On(RenameClient command, CommandContext context) { - return handler.UpdateAsync(context, x => x.RenameClient(command)); + return handler.UpdateAsync(context, a => a.RenameClient(command)); } protected Task On(RevokeClient command, CommandContext context) { - return handler.UpdateAsync(context, x => x.RevokeClient(command)); + return handler.UpdateAsync(context, a => a.RevokeClient(command)); } protected Task On(AddLanguage command, CommandContext context) { - return handler.UpdateAsync(context, x => x.AddLanguage(command)); + return handler.UpdateAsync(context, a => a.AddLanguage(command)); } protected Task On(RemoveLanguage command, CommandContext context) { - return handler.UpdateAsync(context, x => x.RemoveLanguage(command)); + return handler.UpdateAsync(context, a => a.RemoveLanguage(command)); } protected Task On(SetMasterLanguage command, CommandContext context) { - return handler.UpdateAsync(context, x => x.SetMasterLanguage(command)); + return handler.UpdateAsync(context, a => a.SetMasterLanguage(command)); } public Task HandleAsync(CommandContext context) diff --git a/src/Squidex.Write/Contents/ContentCommandHandler.cs b/src/Squidex.Write/Contents/ContentCommandHandler.cs index c0f4f1b9d..aced97828 100644 --- a/src/Squidex.Write/Contents/ContentCommandHandler.cs +++ b/src/Squidex.Write/Contents/ContentCommandHandler.cs @@ -42,9 +42,14 @@ namespace Squidex.Write.Contents protected async Task On(CreateContent command, CommandContext context) { - await ValidateAsync(command, () => "Failed to create content"); + await ValidateAsync(command, () => "Failed to create content", true); - await handler.CreateAsync(context, c => c.Create(command)); + await handler.CreateAsync(context, c => + { + c.Create(command); + + context.Succeed(EntityCreatedResult.Create(command.Data, c.Version)); + }); } protected async Task On(UpdateContent command, CommandContext context) @@ -81,15 +86,13 @@ namespace Squidex.Write.Contents return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } - private async Task ValidateAsync(ContentDataCommand command, Func message) + private async Task ValidateAsync(ContentDataCommand command, Func message, bool enrich = false) { Guard.Valid(command, nameof(command), message); - var taskForApp = - appProvider.FindAppByIdAsync(command.AppId.Id); + var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id); - var taskForSchema = - schemas.FindSchemaByIdAsync(command.SchemaId.Id); + var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id); await Task.WhenAll(taskForApp, taskForSchema); @@ -100,12 +103,15 @@ namespace Squidex.Write.Contents await schemaObject.ValidateAsync(command.Data, schemaErrors, languages); - schemaObject.Enrich(command.Data, languages); - if (schemaErrors.Count > 0) { throw new ValidationException(message(), schemaErrors); } + + if (enrich) + { + schemaObject.Enrich(command.Data, languages); + } } } } diff --git a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs index 7bcd2cc34..d4af27a0f 100644 --- a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs +++ b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs @@ -42,7 +42,12 @@ namespace Squidex.Write.Schemas throw new ValidationException("Cannot create a new schema", error); } - await handler.CreateAsync(context, s => s.Create(command)); + await handler.CreateAsync(context, s => + { + s.Create(command); + + context.Succeed(EntityCreatedResult.Create(s.Id, s.Version)); + }); } protected Task On(AddField command, CommandContext context) @@ -51,7 +56,7 @@ namespace Squidex.Write.Schemas { s.AddField(command); - context.Succeed(new EntityCreatedResult(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id, s.Version)); + context.Succeed(EntityCreatedResult.Create(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id, s.Version)); }); } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 0c1d23e07..cc002f906 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.Api; using Squidex.Controllers.ContentApi.Models; using Squidex.Core.Contents; using Squidex.Core.Identity; @@ -122,10 +121,12 @@ namespace Squidex.Controllers.ContentApi var context = await CommandBus.PublishAsync(command); - var result = context.Result>().IdOrValue; - var response = new EntityCreatedDto { Id = result.ToString() }; + var result = context.Result>(); + var response = ContentDto.Create(command, result); - return CreatedAtAction(nameof(GetContent), new { id = result }, response); + Response.Headers["ETag"] = new StringValues(response.Version.ToString()); + + return CreatedAtAction(nameof(GetContent), new { id = response.Id }, response); } [HttpPut] diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 10bd02d85..c3c4e5e8f 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -44,7 +44,6 @@ namespace Squidex.Controllers.ContentApi.Generator private readonly string schemaBodyDescription; private HashSet languages; private JsonSchema4 errorDtoSchema; - private JsonSchema4 entityCreatedDtoSchema; private string appBasePath; private IAppEntity app; @@ -137,11 +136,6 @@ namespace Squidex.Controllers.ContentApi.Generator var errorSchema = JsonObjectTypeDescription.FromType(errorType, new Attribute[0], EnumHandling.String); errorDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null); - - var entityCreatedType = typeof(EntityCreatedDto); - var entityCreatedSchema = JsonObjectTypeDescription.FromType(entityCreatedType, new Attribute[0], EnumHandling.String); - - entityCreatedDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(entityCreatedType, entityCreatedSchema.IsNullable, null); } private void GenerateSecurityRequirements() @@ -269,8 +263,10 @@ namespace Squidex.Controllers.ContentApi.Generator operation.Summary = $"Create a {schemaName} content."; + var responseSchema = CreateContentSchema(schemaName, schemaIdentifier, dataSchema); + operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("201", $"{schemaName} created.", entityCreatedDtoSchema); + operation.AddResponse("201", $"{schemaName} created.", responseSchema); }); } @@ -380,22 +376,19 @@ namespace Squidex.Controllers.ContentApi.Generator private JsonSchema4 CreateContentSchema(string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { - var CreateProperty = - new Func((d, f) => - new JsonProperty { Description = d, Format = f, IsRequired = true, Type = JsonObjectType.String }); - var dataProperty = new JsonProperty { Description = schemaBodyDescription, Type = JsonObjectType.Object, IsRequired = true, SchemaReference = dataSchema }; var schema = new JsonSchema4 { Properties = { - ["id"] = CreateProperty($"The id of the {schemaName} content.", null), + ["id"] = CreateProperty($"The id of the {schemaName} content."), ["data"] = dataProperty, + ["version"] = CreateProperty($"The version of the {schemaName}", JsonObjectType.Number), ["created"] = CreateProperty($"The date and time when the {schemaName} content has been created.", "date-time"), - ["createdBy"] = CreateProperty($"The user that has created the {schemaName} content.", null), + ["createdBy"] = CreateProperty($"The user that has created the {schemaName} content."), ["lastModified"] = CreateProperty($"The date and time when the {schemaName} content has been modified last.", "date-time"), - ["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.", null) + ["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.") }, Type = JsonObjectType.Object }; @@ -403,6 +396,16 @@ namespace Squidex.Controllers.ContentApi.Generator return AppendSchema($"{schemaIdentifier}ContentDto", schema); } + private static JsonProperty CreateProperty(string description, JsonObjectType type) + { + return new JsonProperty { Description = description, IsRequired = true, Type = type }; + } + + private static JsonProperty CreateProperty(string description, string format = null) + { + return new JsonProperty { Description = description, Format = format, IsRequired = true, Type = JsonObjectType.String }; + } + private JsonSchema4 AppendSchema(string name, JsonSchema4 schema) { name = char.ToUpperInvariant(name[0]) + name.Substring(1); diff --git a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs index ae09e993d..a5fd1e83e 100644 --- a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs +++ b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs @@ -9,7 +9,10 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; +using Squidex.Core.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Write.Contents.Commands; namespace Squidex.Controllers.ContentApi.Models { @@ -56,6 +59,24 @@ namespace Squidex.Controllers.ContentApi.Models /// /// The version of the content. /// - public int Version { get; set; } + public long Version { get; set; } + + public static ContentDto Create(CreateContent command, EntityCreatedResult result) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var response = new ContentDto + { + Id = command.ContentId, + Data = result.IdOrValue, + Version = result.Version, + Created = now, + CreatedBy = command.Actor, + LastModified = now, + LastModifiedBy = command.Actor + }; + + return response; + } } } diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 0b15b1e4a..cd0d6e0cb 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -102,7 +102,7 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, this.appName() .switchMap(app => this.contentsService.postContent(app, this.schema.name, data, this.version)) .subscribe(created => { - this.messageBus.publish(new ContentCreated(created.id, data, this.version.value)); + this.messageBus.publish(new ContentCreated(created.id, created.data, this.version.value)); this.router.navigate(['../'], { relativeTo: this.route }); }, error => { diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index d7f693a04..eae9f9af7 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -143,6 +143,9 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { this.contentItems = this.contentItems.removeAll(x => x.id === content.id); + this.contentTotal--; + + this.updatePaging(); this.messageBus.publish(new ContentDeleted(content.id)); }, error => { diff --git a/src/Squidex/app/framework/angular/http-utils.ts b/src/Squidex/app/framework/angular/http-utils.ts index 3aad1e690..2dec6e0de 100644 --- a/src/Squidex/app/framework/angular/http-utils.ts +++ b/src/Squidex/app/framework/angular/http-utils.ts @@ -54,14 +54,17 @@ export function catchError(message: string): Observable { let result = new ErrorDto(500, message); if (error instanceof Response) { - const body = error.json(); - - if (error.status === 412) { - result = new ErrorDto(error.status, 'Failed to make the update. Another user has made a change. Please reload.'); - } else if (error.status !== 500) { - result = new ErrorDto(error.status, body.message, body.details); + try { + const body = error.json(); + + if (error.status === 412) { + result = new ErrorDto(error.status, 'Failed to make the update. Another user has made a change. Please reload.'); + } else if (error.status !== 500) { + result = new ErrorDto(error.status, body.message, body.details); + } + } catch (e) { + result = result; } - } return Observable.throw(result); diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 45def41b2..6aacdf637 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -12,7 +12,6 @@ import { IMock, It, Mock, Times } from 'typemoq'; import { ApiUrlConfig, AuthService, - EntityCreatedDto, ContentDto, ContentsDto, ContentsService, @@ -173,21 +172,32 @@ describe('ContentsService', () => { new Response( new ResponseOptions({ body: { - id: 'content1' + id: 'id1', + isPublished: true, + created: '2016-12-12T10:10', + createdBy: 'Created1', + lastModified: '2017-12-12T10:10', + lastModifiedBy: 'LastModifiedBy1', + version: 11, + data: {} } }) ) )) .verifiable(Times.once()); - let created: EntityCreatedDto | null = null; + let content: ContentDto | null = null; contentsService.postContent('my-app', 'my-schema', dto, version).subscribe(result => { - created = result; + content = result; }); - expect(created).toEqual( - new EntityCreatedDto('content1')); + expect(content).toEqual( + new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + {}, + new Version('11'))); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 69614d5c5..a7cf32983 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -13,7 +13,6 @@ import 'framework/angular/http-extensions'; import { ApiUrlConfig, DateTime, - EntityCreatedDto, Version } from 'framework'; @@ -111,13 +110,21 @@ export class ContentsService { .catchError('Failed to load content. Please reload.'); } - public postContent(appName: string, schemaName: string, dto: any, version: Version): Observable { + public postContent(appName: string, schemaName: string, dto: any, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}`); return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { - return new EntityCreatedDto(response.id); + return new ContentDto( + response.id, + response.isPublished, + response.createdBy, + response.lastModifiedBy, + DateTime.parseISO_UTC(response.created), + DateTime.parseISO_UTC(response.lastModified), + response.data, + new Version(response.version.toString())); }) .catchError('Failed to create content. Please reload.'); } diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index 2d36e6f38..c99271606 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using FluentAssertions; using Moq; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Read.Apps; using Squidex.Read.Apps.Repositories; using Squidex.Read.Users; @@ -74,7 +75,7 @@ namespace Squidex.Write.Apps await sut.HandleAsync(context); }); - Assert.Equal(AppId, context.Result()); + Assert.Equal(AppId, context.Result>().IdOrValue); } [Fact] @@ -154,7 +155,7 @@ namespace Squidex.Write.Apps keyGenerator.VerifyAll(); - context.Result().ShouldBeEquivalentTo(new AppClient(clientName, clientSecret)); + context.Result>().IdOrValue.ShouldBeEquivalentTo(new AppClient(clientName, clientSecret)); } [Fact] diff --git a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs index 681471797..1a2d3cbe4 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -12,6 +12,7 @@ using Moq; using Squidex.Core.Contents; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Read.Apps; using Squidex.Read.Apps.Services; using Squidex.Read.Schemas; @@ -73,6 +74,8 @@ namespace Squidex.Write.Contents { await sut.HandleAsync(context); }); + + Assert.Equal(data, context.Result>().IdOrValue); } [Fact] diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index bc9a93699..80cd0b6a5 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; using Moq; using Squidex.Core.Schemas; @@ -66,6 +67,8 @@ namespace Squidex.Write.Schemas { await sut.HandleAsync(context); }); + + Assert.Equal(SchemaId, context.Result>().IdOrValue); } [Fact]