From 595ee923ca91da281ea0d56796a4e4c430ee2756 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 28 Feb 2017 11:43:45 +0100 Subject: [PATCH 01/66] Update schema-page.component.ts --- .../app/features/schemas/pages/schema/schema-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index a64df5f9b..af05b18ea 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -59,7 +59,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public addFieldFormSubmitted = false; public addFieldForm: FormGroup = this.formBuilder.group({ - type: ['string', + type: ['String', [ Validators.required ]], From b795c9f72f9a643f8eec9e44e5ee6f024a0de770 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 28 Feb 2017 11:44:19 +0100 Subject: [PATCH 02/66] Update schema-page.component.ts --- .../app/features/schemas/pages/schema/schema-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index af05b18ea..f1ec3ff3e 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -59,7 +59,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public addFieldFormSubmitted = false; public addFieldForm: FormGroup = this.formBuilder.group({ - type: ['String', + type: [this.fieldTypes[0]', [ Validators.required ]], From f078a009c56458b808c70e0faf5c61bb5cd56f9a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 28 Feb 2017 11:44:39 +0100 Subject: [PATCH 03/66] Update schema-page.component.ts --- .../app/features/schemas/pages/schema/schema-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index f1ec3ff3e..d3289f38b 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -59,7 +59,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public addFieldFormSubmitted = false; public addFieldForm: FormGroup = this.formBuilder.group({ - type: [this.fieldTypes[0]', + type: [this.fieldTypes[0], [ Validators.required ]], From bd57ca558a95f3f32ec21a31ff1bfaf3479e551a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 Feb 2017 19:14:41 +0100 Subject: [PATCH 04/66] Log path fixed --- src/Squidex/web.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/web.config b/src/Squidex/web.config index 142605f6f..3622c3223 100644 --- a/src/Squidex/web.config +++ b/src/Squidex/web.config @@ -4,6 +4,6 @@ - + \ No newline at end of file From 08ccf7d396ce8cc1abd8b2adb6af9d94f8a618b9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 Feb 2017 19:37:13 +0100 Subject: [PATCH 05/66] Default settings --- src/Squidex/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index c97e21e19..6b92e4a90 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -4,7 +4,7 @@ "baseUrl": "http://localhost:5000" }, "clusterer": { - "type": "redis", + "type": "none", "redis": { "connectionString": "localhost:6379,resolveDns=1" } From 8501db36b95832b4cb1bf618b3800534f9165dc3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 Feb 2017 21:29:28 +0100 Subject: [PATCH 06/66] Small spelling error. --- .../ContentApi/Generator/SchemasSwaggerGenerator.cs | 4 ++-- .../features/schemas/pages/schemas/schema-form.component.html | 2 +- .../features/schemas/pages/schemas/schema-form.component.ts | 2 ++ src/Squidex/app/shared/components/app-form.component.html | 2 +- src/Squidex/app/shared/components/app-form.component.ts | 3 ++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 89f76fa9f..e93b1f57c 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -165,7 +165,7 @@ namespace Squidex.Controllers.ContentApi.Generator private void GenerateSchemasOperations(IEnumerable schemas) { - foreach (var schema in schemas.Select(x => x.Schema)) + foreach (var schema in schemas.Where(x => x.IsPublished).Select(x => x.Schema)) { GenerateSchemaOperations(schema); } @@ -310,7 +310,7 @@ namespace Squidex.Controllers.ContentApi.Generator if (entityName != null) { - operation.AddPathParameter("id", JsonObjectType.String, $"The id of the {entityName} (GUID)."); + operation.AddPathParameter("id", JsonObjectType.String, $"The id of the {entityName} content (GUID)."); operation.AddResponse("404", $"App, schema or {entityName} content not found."); } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html index 8ff470085..4e3fd75c2 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html @@ -13,7 +13,7 @@ - The schema name becomes part of the api url,
e.g https://{{appName}}.squidex.io/{{schemaName | async}}/. + The schema name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName}}/{{schemaName | async}}/.
It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later. diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts index 8c24bcc55..cf3ffcfec 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts @@ -10,6 +10,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; import { + ApiUrlConfig, AuthService, CreateSchemaDto, DateTime, @@ -56,6 +57,7 @@ export class SchemaFormComponent { .merge(this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME)); constructor( + public readonly apiUrl: ApiUrlConfig, private readonly schemas: SchemasService, private readonly formBuilder: FormBuilder, private readonly authService: AuthService diff --git a/src/Squidex/app/shared/components/app-form.component.html b/src/Squidex/app/shared/components/app-form.component.html index 1855817c8..905aa2e59 100644 --- a/src/Squidex/app/shared/components/app-form.component.html +++ b/src/Squidex/app/shared/components/app-form.component.html @@ -13,7 +13,7 @@ - The app name becomes part of the api url,
e.g https://{{appName | async}}.squidex.io/. + The app name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName | async}}.squidex.io/.
diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/src/Squidex/app/shared/components/app-form.component.ts index 5bf4f1bcc..3866aa5d3 100644 --- a/src/Squidex/app/shared/components/app-form.component.ts +++ b/src/Squidex/app/shared/components/app-form.component.ts @@ -9,7 +9,7 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; -import { ValidatorsEx } from 'framework'; +import { ApiUrlConfig, ValidatorsEx } from 'framework'; import { AppsStoreService } from './../services/apps-store.service'; import { AppDto, CreateAppDto } from './../services/apps.service'; @@ -45,6 +45,7 @@ export class AppFormComponent { .merge(this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME)); constructor( + public readonly apiUrl: ApiUrlConfig, private readonly appsStore: AppsStoreService, private readonly formBuilder: FormBuilder ) { From e672d98fab5c6f363b4b27288a0ae6f89e8b7c53 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 Feb 2017 21:49:19 +0100 Subject: [PATCH 07/66] Minor css fixes --- .../features/schemas/pages/schemas/schemas-page.component.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss index 8c4e6de75..36b77b2c7 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss @@ -27,6 +27,9 @@ $button-size: calc(2.5rem - 2px); &-modified { text-align: right; + width: auto; + white-space: nowrap; + padding-left: 0; } &-user { From ef9fa4a7890eec166d5028a86e264153cca91c1d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 Feb 2017 22:02:01 +0100 Subject: [PATCH 08/66] Fixes in swagger definition --- src/Squidex/Pipeline/Swagger/SwaggerHelper.cs | 2 -- src/Squidex/app/shared/components/app-form.component.html | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs index 7494341d8..a7c1ff130 100644 --- a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs +++ b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs @@ -83,8 +83,6 @@ namespace Squidex.Pipeline.Swagger parameter.IsNullableRaw = false; operation.Parameters.Add(parameter); - - operation.Parameters.Add(parameter); } public static void AddBodyParameter(this SwaggerOperation operation, JsonSchema4 schema, string name, string description) diff --git a/src/Squidex/app/shared/components/app-form.component.html b/src/Squidex/app/shared/components/app-form.component.html index 905aa2e59..77c9a57e3 100644 --- a/src/Squidex/app/shared/components/app-form.component.html +++ b/src/Squidex/app/shared/components/app-form.component.html @@ -13,7 +13,7 @@ - The app name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName | async}}.squidex.io/. + The app name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName | async}}/.
From 76e319a59316b185b47c58029d8948e63366cf25 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 Feb 2017 22:51:19 +0100 Subject: [PATCH 09/66] Another fix in the swagger definition: OperationId had a random generated name. --- .../Generator/SchemasSwaggerGenerator.cs | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index e93b1f57c..2f62af27e 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -173,26 +173,27 @@ namespace Squidex.Controllers.ContentApi.Generator private void GenerateSchemaOperations(Schema schema) { - var schemaName = schema.Properties.Label ?? schema.Name; + var schemaIdentifier = schema.Name.ToPascalCase(); + var schemaName = !string.IsNullOrWhiteSpace(schema.Properties.Label) ? schema.Properties.Label.Trim() : schema.Name; document.Tags.Add( new SwaggerTag { - Name = schemaName, Description = $"API to managed {schemaName} content." + Name = schemaName, Description = $"API to managed {schemaName} contents." }); - var dataSchem = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema)); + var dataSchema = AppendSchema($"{schemaIdentifier}Dto", schema.BuildSchema(languages, AppendSchema)); var schemaOperations = new List { - GenerateSchemaQueryOperation(schema, schemaName, dataSchem), - GenerateSchemaCreateOperation(schema, schemaName, dataSchem), - GenerateSchemaGetOperation(schema, schemaName, dataSchem), - GenerateSchemaUpdateOperation(schema, schemaName, dataSchem), - GenerateSchemaPatchOperation(schema, schemaName, dataSchem), - GenerateSchemaPublishOperation(schema, schemaName), - GenerateSchemaUnpublishOperation(schema, schemaName), - GenerateSchemaDeleteOperation(schema, schemaName) + GenerateSchemaQueryOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaCreateOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaGetOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaUpdateOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaPatchOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaPublishOperation(schema, schemaName, schemaIdentifier), + GenerateSchemaUnpublishOperation(schema, schemaName, schemaIdentifier), + GenerateSchemaDeleteOperation(schema, schemaName, schemaIdentifier) }; foreach (var operation in schemaOperations.SelectMany(x => x.Values).Distinct()) @@ -201,11 +202,13 @@ namespace Squidex.Controllers.ContentApi.Generator } } - private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation => { - operation.Summary = $"Queries {schemaName} content."; + operation.OperationId = $"Query{schemaIdentifier}Contents"; + + operation.Summary = $"Queries {schemaName} contents."; operation.Description = schemaQueryDescription; @@ -221,78 +224,89 @@ namespace Squidex.Controllers.ContentApi.Generator }); } - private SwaggerOperations GenerateSchemaGetOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaGetOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => { + operation.OperationId = $"Get{schemaIdentifier}Content"; + operation.Summary = $"Get a {schemaName} content."; - var responseSchema = CreateContentSchema(schemaName, schema.Name, dataSchema); + var responseSchema = CreateContentSchema(schemaName, schemaIdentifier, dataSchema); operation.AddResponse("200", $"{schemaName} content found.", responseSchema); }); } - private SwaggerOperations GenerateSchemaCreateOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaCreateOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Post, null, $"{appBasePath}/{schema.Name}", operation => { + operation.OperationId = $"Create{schemaIdentifier}Content"; + operation.Summary = $"Create a {schemaName} content."; operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("201", $"{schemaName} created.", entityCreatedDtoSchema); }); } - private SwaggerOperations GenerateSchemaUpdateOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaUpdateOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => { + operation.OperationId = $"Update{schemaIdentifier}Content"; + operation.Summary = $"Update a {schemaName} content."; operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("204", $"{schemaName} element updated."); }); } - private SwaggerOperations GenerateSchemaPatchOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaPatchOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => { + operation.OperationId = $"Path{schemaIdentifier}Content"; + operation.Summary = $"Patchs a {schemaName} content."; operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("204", $"{schemaName} element updated."); }); } - private SwaggerOperations GenerateSchemaPublishOperation(Schema schema, string schemaName) + private SwaggerOperations GenerateSchemaPublishOperation(Schema schema, string schemaName, string schemaIdentifier) { return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/publish", operation => { + operation.OperationId = $"Publish{schemaIdentifier}Content"; + operation.Summary = $"Publish a {schemaName} content."; operation.AddResponse("204", $"{schemaName} element published."); }); } - private SwaggerOperations GenerateSchemaUnpublishOperation(Schema schema, string schemaName) + private SwaggerOperations GenerateSchemaUnpublishOperation(Schema schema, string schemaName, string schemaIdentifier) { return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/unpublish", operation => { + operation.OperationId = $"Unpublish{schemaIdentifier}Content"; + operation.Summary = $"Unpublish a {schemaName} content."; operation.AddResponse("204", $"{schemaName} element unpublished."); }); } - private SwaggerOperations GenerateSchemaDeleteOperation(Schema schema, string schemaName) + private SwaggerOperations GenerateSchemaDeleteOperation(Schema schema, string schemaName, string schemaIdentifier) { return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/", operation => { + operation.OperationId = $"Delete{schemaIdentifier}Content"; + operation.Summary = $"Delete a {schemaName} content."; operation.AddResponse("204", $"{schemaName} content deleted."); @@ -341,7 +355,7 @@ namespace Squidex.Controllers.ContentApi.Generator return schema; } - private JsonSchema4 CreateContentSchema(string schemaName, string id, JsonSchema4 dataSchema) + private JsonSchema4 CreateContentSchema(string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { var CreateProperty = new Func((d, f) => @@ -363,7 +377,7 @@ namespace Squidex.Controllers.ContentApi.Generator Type = JsonObjectType.Object }; - return AppendSchema($"{id}ContentDto", schema); + return AppendSchema($"{schemaIdentifier}ContentDto", schema); } private JsonSchema4 AppendSchema(string name, JsonSchema4 schema) From ed951dd98800a43b1906395cbaf42ba2f66859bb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 1 Mar 2017 00:06:53 +0100 Subject: [PATCH 10/66] Some swagger fixes --- src/Squidex.Core/Schemas/Field.cs | 2 +- src/Squidex.Core/Schemas/Schema.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 5f5636b94..d657b0e53 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -197,7 +197,7 @@ namespace Squidex.Core.Schemas languagesObject.Properties.Add(language.Iso2Code, languageProperty); } - languagesProperty.AllOf.Add(schemaResolver($"{schemaName}{Name.ToPascalCase()}Property", languagesObject)); + languagesProperty.SchemaReference = schemaResolver($"{schemaName}{Name.ToPascalCase()}Property", languagesObject); schema.Properties.Add(Name, languagesProperty); } diff --git a/src/Squidex.Core/Schemas/Schema.cs b/src/Squidex.Core/Schemas/Schema.cs index dba1f78b9..8832374d0 100644 --- a/src/Squidex.Core/Schemas/Schema.cs +++ b/src/Squidex.Core/Schemas/Schema.cs @@ -196,7 +196,7 @@ namespace Squidex.Core.Schemas var schemaName = Name.ToPascalCase(); - var schema = new JsonSchema4 { Id = schemaName, Type = JsonObjectType.Object }; + var schema = new JsonSchema4 { Type = JsonObjectType.Object }; foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden)) { @@ -300,7 +300,7 @@ namespace Squidex.Core.Schemas } } - private static async Task ValidateLocalizableFieldAsync(HashSet languages, ContentFieldData fieldData, List fieldErrors, Field field) + private static async Task ValidateLocalizableFieldAsync(ICollection languages, ContentFieldData fieldData, List fieldErrors, Field field) { foreach (var valueLanguage in fieldData.Keys) { From ab3681e67d4dbb6a926573773393c04bf2172a00 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 1 Mar 2017 08:06:38 +0100 Subject: [PATCH 11/66] More swagger improvements --- src/Squidex.Core/Schemas/Schema.cs | 6 +++--- src/Squidex/Config/Constants.cs | 2 ++ src/Squidex/Config/Swagger/SwaggerServices.cs | 4 ++-- .../ContentApi/Generator/SchemasSwaggerGenerator.cs | 2 +- src/Squidex/Pipeline/Swagger/SwaggerHelper.cs | 7 ++++++- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Squidex.Core/Schemas/Schema.cs b/src/Squidex.Core/Schemas/Schema.cs index 8832374d0..b27427091 100644 --- a/src/Squidex.Core/Schemas/Schema.cs +++ b/src/Squidex.Core/Schemas/Schema.cs @@ -289,7 +289,7 @@ namespace Squidex.Core.Schemas } } - private void ValidateUnknownFields(ContentData data, IList errors) + private void ValidateUnknownFields(ContentData data, ICollection errors) { foreach (var fieldData in data) { @@ -300,7 +300,7 @@ namespace Squidex.Core.Schemas } } - private static async Task ValidateLocalizableFieldAsync(ICollection languages, ContentFieldData fieldData, List fieldErrors, Field field) + private static async Task ValidateLocalizableFieldAsync(ICollection languages, ContentFieldData fieldData, ICollection fieldErrors, Field field) { foreach (var valueLanguage in fieldData.Keys) { @@ -322,7 +322,7 @@ namespace Squidex.Core.Schemas } } - private static async Task ValidateNonLocalizableField(ContentFieldData fieldData, List fieldErrors, Field field) + private static async Task ValidateNonLocalizableField(ContentFieldData fieldData, ICollection fieldErrors, Field field) { if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code)) { diff --git a/src/Squidex/Config/Constants.cs b/src/Squidex/Config/Constants.cs index 0c994fb2a..dbf547316 100644 --- a/src/Squidex/Config/Constants.cs +++ b/src/Squidex/Config/Constants.cs @@ -10,6 +10,8 @@ namespace Squidex.Config { public class Constants { + public static readonly string SecurityDefinition = "oauth-client-auth"; + public static readonly string ApiPrefix = "/api"; public static readonly string ApiScope = "squidex-api"; diff --git a/src/Squidex/Config/Swagger/SwaggerServices.cs b/src/Squidex/Config/Swagger/SwaggerServices.cs index 5a0c5ee28..75ac816fd 100644 --- a/src/Squidex/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Config/Swagger/SwaggerServices.cs @@ -43,9 +43,9 @@ namespace Squidex.Config.Swagger private static SwaggerOwinSettings ConfigureIdentity(this SwaggerOwinSettings settings, MyUrlsOptions urlOptions) { settings.DocumentProcessors.Add( - new SecurityDefinitionAppender("OAuth2", SwaggerHelper.CreateOAuthSchema(urlOptions))); + new SecurityDefinitionAppender(Constants.SecurityDefinition, SwaggerHelper.CreateOAuthSchema(urlOptions))); - settings.OperationProcessors.Add(new OperationSecurityScopeProcessor("roles")); + settings.OperationProcessors.Add(new OperationSecurityScopeProcessor(Constants.SecurityDefinition)); return settings; } diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 2f62af27e..010fafcc7 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -145,7 +145,7 @@ namespace Squidex.Controllers.ContentApi.Generator { new SwaggerSecurityRequirement { - { "roles", new List { SquidexRoles.AppOwner, SquidexRoles.AppDeveloper, SquidexRoles.AppEditor } } + { Constants.SecurityDefinition, new List { SquidexRoles.AppOwner, SquidexRoles.AppDeveloper, SquidexRoles.AppEditor } } } }; diff --git a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs index a7c1ff130..4c27af4d7 100644 --- a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs +++ b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs @@ -13,6 +13,7 @@ using NJsonSchema; using NSwag; using Squidex.Config; using System.Reflection; +using Squidex.Core.Identity; namespace Squidex.Pipeline.Swagger { @@ -46,11 +47,15 @@ namespace Squidex.Pipeline.Swagger new SwaggerSecurityScheme { TokenUrl = tokenUrl, + Name = Constants.SecurityDefinition, Type = SwaggerSecuritySchemeType.OAuth2, Flow = SwaggerOAuth2Flow.Application, Scopes = new Dictionary { - { Constants.ApiScope, "Read and write access to the API" } + { Constants.ApiScope, "Read and write access to the API" }, + { SquidexRoles.AppOwner, "You get this scope / role when you are owner of the app you are accessing." }, + { SquidexRoles.AppEditor, "You get this scope / role when you are owner of the app you are accessing or when the subject is a client." }, + { SquidexRoles.AppDeveloper, "You get this scope / role when you are owner of the app you are accessing." } }, Description = securityDescription }; From af37af81b5e7189227eaef155f4bb6cb747937f5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 1 Mar 2017 08:59:49 +0100 Subject: [PATCH 12/66] Name removed --- src/Squidex/Pipeline/Swagger/SwaggerHelper.cs | 1 - src/Squidex/Squidex.csproj | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs index 4c27af4d7..926b961c3 100644 --- a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs +++ b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs @@ -47,7 +47,6 @@ namespace Squidex.Pipeline.Swagger new SwaggerSecurityScheme { TokenUrl = tokenUrl, - Name = Constants.SecurityDefinition, Type = SwaggerSecuritySchemeType.OAuth2, Flow = SwaggerOAuth2Flow.Application, Scopes = new Dictionary diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index e37dd2817..16f3012af 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -68,5 +68,11 @@ + + + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.Web.dll + + From 5ffedb5fdc012efa809cec04a22bcdd1855d1ab4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 1 Mar 2017 19:45:24 +0100 Subject: [PATCH 13/66] Fix in API validation --- src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs index 878968f2b..2345c38f8 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs @@ -18,7 +18,7 @@ namespace Squidex.Controllers.Api.Schemas.Models /// The name of the field. Must be unique within the schema. /// [Required] - [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + [RegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] public string Name { get; set; } /// From 2c9805e7549380e1a182169280733fb5d9a7dbe8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 1 Mar 2017 20:20:06 +0100 Subject: [PATCH 14/66] Minor fix for schema generation. --- src/Squidex.Core/Schemas/Field.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index d657b0e53..967d79848 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -206,9 +206,9 @@ namespace Squidex.Core.Schemas { var jsonProperty = new JsonProperty { IsRequired = RawProperties.IsRequired, Type = JsonObjectType.Object }; - if (!string.IsNullOrWhiteSpace(RawProperties.Label)) + if (!string.IsNullOrWhiteSpace(RawProperties.Hints)) { - jsonProperty.Description = RawProperties.Label; + jsonProperty.Description = RawProperties.Hints; } else { From 56f5038de882b553c901fd4b1e4a89aaa16e3e99 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 1 Mar 2017 21:35:59 +0100 Subject: [PATCH 15/66] Squidex-icon for doku --- .../ContentApi/Generator/SchemasSwaggerGenerator.cs | 6 +++++- src/Squidex/Views/Shared/Docs.cshtml | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 010fafcc7..140bb0b8d 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -104,7 +104,11 @@ namespace Squidex.Controllers.ContentApi.Generator { document.Info = new SwaggerInfo { - Title = $"Suidex API for {app.Name} App" + ExtensionData = new Dictionary + { + ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } + }, + Title = $"Suidex API for {app.Name} App", }; } diff --git a/src/Squidex/Views/Shared/Docs.cshtml b/src/Squidex/Views/Shared/Docs.cshtml index 82c5642bc..48ce58060 100644 --- a/src/Squidex/Views/Shared/Docs.cshtml +++ b/src/Squidex/Views/Shared/Docs.cshtml @@ -14,9 +14,15 @@ h1, .menu-header, .menu-cat-header:hover, - .menu-cat-header.active { + .menu-cat-header.active, + .menu-item-depth-1 > .menu-item-header:not(.disabled):hover, + .menu-item-depth-1.active > .menu-item-header { color: #3f83df !important; } + + .method-content a { + color: #3d7dd5 !important; + } From ca03fe6ea7b1d3ee099abb4d14769c14a4c83b19 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 1 Mar 2017 21:36:19 +0100 Subject: [PATCH 16/66] Icons were not pushed --- media/logo-white.png | Bin 0 -> 9992 bytes media/logo-white.svg | 112 ++++++++++++++++++++++ src/Squidex/wwwroot/images/logo-white.png | Bin 0 -> 10809 bytes 3 files changed, 112 insertions(+) create mode 100644 media/logo-white.png create mode 100644 media/logo-white.svg create mode 100644 src/Squidex/wwwroot/images/logo-white.png diff --git a/media/logo-white.png b/media/logo-white.png new file mode 100644 index 0000000000000000000000000000000000000000..35960bba613693f0c7331fccfacc329a2249214b GIT binary patch literal 9992 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 + + +image/svg+xml \ No newline at end of file diff --git a/src/Squidex/wwwroot/images/logo-white.png b/src/Squidex/wwwroot/images/logo-white.png new file mode 100644 index 0000000000000000000000000000000000000000..157239d5b61a5397600f86f7c9f04373e8c9e923 GIT binary patch literal 10809 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 literal 0 HcmV?d00001 From 4c5f00ddbdcab0184d3885617277179717b80f61 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 3 Mar 2017 19:06:39 +0100 Subject: [PATCH 17/66] ETag support --- .../MongoRepositoryBase.cs | 3 +- .../CQRS/Commands/AggregateHandler.cs | 60 ++++---- .../CQRS/Commands/CommandingExtensions.cs | 17 +-- .../Commands/DefaultDomainObjectRepository.cs | 8 +- .../Commands/EnrichWithTimestampHandler.cs | 3 +- .../CQRS/Commands/EntityCreatedResult.cs | 21 +++ .../EntitySavedResult.cs} | 16 ++- .../CQRS/Commands/IAggregateHandler.cs | 4 +- .../CQRS/Commands/ICommand.cs | 1 + .../CQRS/Commands/IDomainObjectRepository.cs | 2 +- .../CQRS/Commands/LogExceptionHandler.cs | 3 +- .../CQRS/Commands/LogExecutingHandler.cs | 3 +- .../Events/EnrichWithAggregateIdProcessor.cs | 29 ---- .../DomainObjectVersionException.cs | 12 +- .../Tasks/TaskHelper.cs | 11 ++ .../Apps/MongoAppEntity.cs | 4 + .../Contents/MongoContentEntity.cs | 4 + .../History/MongoHistoryEventEntity.cs | 4 +- .../Schemas/MongoSchemaEntity.cs | 4 + .../Utils/EntityMapper.cs | 24 +++- src/Squidex.Read/Apps/IAppEntity.cs | 2 +- src/Squidex.Read/Contents/IContentEntity.cs | 2 +- ...tedByEntity.cs => IEntityWithCreatedBy.cs} | 2 +- ...Entity.cs => IEntityWithLastModifiedBy.cs} | 2 +- src/Squidex.Read/IEntityWithVersion.cs | 15 ++ src/Squidex.Read/Schemas/ISchemaEntity.cs | 2 +- src/Squidex.Write/Apps/AppCommandHandler.cs | 25 ++-- .../Contents/ContentCommandHandler.cs | 20 ++- .../Schemas/SchemaCommandHandler.cs | 34 ++--- src/Squidex.Write/SquidexCommand.cs | 2 + src/Squidex/Config/Domain/WriteModule.cs | 13 +- src/Squidex/Config/Identity/IdentityUsage.cs | 3 +- src/Squidex/Config/Swagger/XmlTagProcessor.cs | 3 +- .../Api/Apps/AppClientsController.cs | 10 +- .../Api/Apps/AppContributorsController.cs | 3 + .../Controllers/Api/Apps/AppController.cs | 6 +- .../Api/Apps/AppLanguagesController.cs | 5 +- .../Controllers/Api/Apps/Models/AppDto.cs | 5 + .../Controllers/Api/Apps/Models/ClientDto.cs | 2 +- .../Controllers/Api/EntityCreatedDto.cs | 7 +- .../Api/Schemas/SchemaFieldsController.cs | 6 +- .../Api/Schemas/SchemasController.cs | 3 + .../ContentApi/ContentsController.cs | 19 ++- .../Generator/SchemasSwaggerGenerator.cs | 21 ++- .../Controllers/ContentApi/PingController.cs | 28 ++++ .../UI/Account/AccountController.cs | 5 +- .../CommandHandlers/EnrichWithActorHandler.cs | 10 +- .../CommandHandlers/EnrichWithAppIdHandler.cs | 3 +- .../EnrichWithExpectedVersionHandler.cs | 40 ++++++ .../SetVersionAsETagHandler.cs | 38 ++++++ src/Squidex/Views/Shared/Docs.cshtml | 2 +- .../CQRS/Commands/AggregateHandlerTests.cs | 128 +++++++----------- .../CQRS/Commands/CommandContextTests.cs | 7 +- .../DefaultDomainObjectRepositoryTests.cs | 7 +- .../EnrichWithTimestampHandlerTests.cs | 8 +- .../CQRS/Commands/InMemoryCommandBusTests.cs | 16 +-- .../CQRS/Commands/LogExceptionHandlerTests.cs | 14 +- .../CQRS/Commands/LogExecutingHandlerTests.cs | 10 +- .../EnrichWithAggregateIdProcessorTests.cs | 56 -------- .../CQRS/Events/EventReceiverTests.cs | 3 +- .../Apps/AppCommandHandlerTests.cs | 12 +- .../Schemas/SchemaCommandHandlerTests.cs | 8 +- .../TestHelpers/HandlerTestBase.cs | 4 +- 63 files changed, 487 insertions(+), 357 deletions(-) create mode 100644 src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs rename src/Squidex.Infrastructure/CQRS/{Events/IEventProcessor.cs => Commands/EntitySavedResult.cs} (54%) delete mode 100644 src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs rename src/Squidex.Read/{ITrackCreatedByEntity.cs => IEntityWithCreatedBy.cs} (91%) rename src/Squidex.Read/{ITrackLastModifiedByEntity.cs => IEntityWithLastModifiedBy.cs} (90%) create mode 100644 src/Squidex.Read/IEntityWithVersion.cs create mode 100644 src/Squidex/Controllers/ContentApi/PingController.cs create mode 100644 src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs create mode 100644 src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs index 481e55b2f..99b5ced58 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs @@ -11,6 +11,7 @@ using System.Globalization; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.MongoDb { @@ -127,7 +128,7 @@ namespace Squidex.Infrastructure.MongoDb protected virtual Task SetupCollectionAsync(IMongoCollection collection) { - return Task.FromResult(true); + return TaskHelper.Done; } public virtual Task ClearAsync() diff --git a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs index 795695484..4bdd4169d 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs @@ -7,9 +7,7 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Infrastructure.CQRS.Commands { @@ -17,7 +15,6 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly IDomainObjectRepository domainObjectRepository; private readonly IDomainObjectFactory domainObjectFactory; - private readonly IEnumerable eventProcessors; public IDomainObjectRepository Repository { @@ -31,55 +28,72 @@ namespace Squidex.Infrastructure.CQRS.Commands public AggregateHandler( IDomainObjectFactory domainObjectFactory, - IDomainObjectRepository domainObjectRepository, - IEnumerable eventProcessors) + IDomainObjectRepository domainObjectRepository) { - Guard.NotNull(eventProcessors, nameof(eventProcessors)); Guard.NotNull(domainObjectFactory, nameof(domainObjectFactory)); Guard.NotNull(domainObjectRepository, nameof(domainObjectRepository)); this.domainObjectFactory = domainObjectFactory; this.domainObjectRepository = domainObjectRepository; - - this.eventProcessors = eventProcessors; } - public async Task CreateAsync(IAggregateCommand command, Func creator) where T : class, IAggregate + public async Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate { Guard.NotNull(creator, nameof(creator)); - Guard.NotNull(command, nameof(command)); - Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId)); + Guard.NotNull(context, nameof(context)); - var aggregate = domainObjectFactory.CreateNew(command.AggregateId); + var aggregateCommand = GetCommand(context); + var aggregate = (T)domainObjectFactory.CreateNew(typeof(T), aggregateCommand.AggregateId); await creator(aggregate); - await Save(command, aggregate); + await SaveAsync(aggregate); + + if (!context.IsHandled) + { + context.Succeed(new EntityCreatedResult(aggregate.Id, aggregate.Version)); + } } - public async Task UpdateAsync(IAggregateCommand command, Func updater) where T : class, IAggregate + public async Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate { Guard.NotNull(updater, nameof(updater)); - Guard.NotNull(command, nameof(command)); - Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId)); + Guard.NotNull(context, nameof(context)); - var aggregate = await domainObjectRepository.GetByIdAsync(command.AggregateId); + var aggregateCommand = GetCommand(context); + var aggregate = await domainObjectRepository.GetByIdAsync(aggregateCommand.AggregateId, aggregateCommand.ExpectedVersion); await updater(aggregate); - await Save(command, aggregate); + await SaveAsync(aggregate); + + if (!context.IsHandled) + { + context.Succeed(new EntitySavedResult(aggregate.Version)); + } + } + + private IAggregateCommand GetCommand(CommandContext context) + { + var command = context.Command as IAggregateCommand; + + if (command == null) + { + throw new ArgumentException("Context must have an aggregate command.", nameof(context)); + } + + Guard.NotEmpty(command.AggregateId, "context.Command.AggregateId"); + + return command; } - private async Task Save(ICommand command, IAggregate aggregate) + private async Task SaveAsync(IAggregate aggregate) { var events = aggregate.GetUncomittedEvents(); foreach (var @event in events) { - foreach (var eventProcessor in eventProcessors) - { - await eventProcessor.ProcessEventAsync(@event, aggregate, command); - } + @event.SetAggregateId(aggregate.Id); } await domainObjectRepository.SaveAsync(aggregate, events, Guid.NewGuid()); diff --git a/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs b/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs index 07b57e503..c56f7599a 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs @@ -14,14 +14,9 @@ namespace Squidex.Infrastructure.CQRS.Commands { public static class CommandingExtensions { - public static T CreateNew(this IDomainObjectFactory factory, Guid id) where T : IAggregate + public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IAggregate { - return (T)factory.CreateNew(typeof(T), id); - } - - public static Task CreateAsync(this IAggregateHandler handler, IAggregateCommand command, Action creator) where T : class, IAggregate - { - return handler.CreateAsync(command, x => + return handler.CreateAsync(context, x => { creator(x); @@ -29,12 +24,12 @@ namespace Squidex.Infrastructure.CQRS.Commands }); } - public static Task UpdateAsync(this IAggregateHandler handler, IAggregateCommand command, Action creator) where T : class, IAggregate + public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IAggregate { - return handler.UpdateAsync(command, x => + return handler.UpdateAsync(context, x => { - creator(x); - + updater(x); + return TaskHelper.Done; }); } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs index 7c657d2e6..517fa6583 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs @@ -39,10 +39,8 @@ namespace Squidex.Infrastructure.CQRS.Commands this.nameResolver = nameResolver; } - public async Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate + public async Task GetByIdAsync(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate { - Guard.GreaterThan(version, 0, nameof(version)); - var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id); var events = await eventStore.GetEventsAsync(streamName).ToList(); @@ -61,9 +59,9 @@ namespace Squidex.Infrastructure.CQRS.Commands domainObject.ApplyEvent(envelope); } - if (domainObject.Version != version && version < int.MaxValue) + if (expectedVersion != null && domainObject.Version != expectedVersion.Value) { - throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, version); + throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, expectedVersion.Value); } return domainObject; diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs index 1f8bc892b..cccfb402a 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using NodaTime; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Commands { @@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.CQRS.Commands timestampCommand.Timestamp = clock.GetCurrentInstant(); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs new file mode 100644 index 000000000..548d83429 --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// EntityCreatedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.CQRS.Commands +{ + public sealed class EntityCreatedResult : EntitySavedResult + { + public T IdOrValue { get; } + + public EntityCreatedResult(T idOrValue, long version) + : base(version) + { + IdOrValue = idOrValue; + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs similarity index 54% rename from src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs rename to src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs index 9a94a51fe..deef50b7c 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs @@ -1,18 +1,20 @@ // ========================================================================== -// IEventProcessor.cs +// EntitySavedResult.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; - -namespace Squidex.Infrastructure.CQRS.Events +namespace Squidex.Infrastructure.CQRS.Commands { - public interface IEventProcessor + public class EntitySavedResult { - Task ProcessEventAsync(Envelope @event, IAggregate aggregate, ICommand command); + public long Version { get; } + + public EntitySavedResult(long version) + { + Version = version; + } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs index 0ebafa736..5d7ca75bb 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs @@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface IAggregateHandler { - Task CreateAsync(IAggregateCommand command, Func creator) where T : class, IAggregate; + Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate; - Task UpdateAsync(IAggregateCommand command, Func updater) where T : class, IAggregate; + Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate; } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs b/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs index c1834662e..fb3516b3b 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs @@ -10,5 +10,6 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface ICommand { + long? ExpectedVersion { get; set; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs index 925076952..2aa0ee679 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface IDomainObjectRepository { - Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate; + Task GetByIdAsync(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate; Task SaveAsync(IAggregate domainObject, ICollection> events, Guid commitId); } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs index e75660e1d..0e31d7548 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -36,7 +37,7 @@ namespace Squidex.Infrastructure.CQRS.Commands logger.LogCritical(InfrastructureErrors.CommandUnknown, exception, "Unknown command {0}", context.Command); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs index 37a4e6d00..d95a02cd6 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Commands { @@ -24,7 +25,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { logger.LogInformation("Handling {0} command", context.Command); - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs b/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs deleted file mode 100644 index e8b07466d..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// EnrichWithAggregateIdProcessor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public sealed class EnrichWithAggregateIdProcessor : IEventProcessor - { - public Task ProcessEventAsync(Envelope @event, IAggregate aggregate, ICommand command) - { - var aggregateCommand = command as IAggregateCommand; - - if (aggregateCommand != null) - { - @event.SetAggregateId(aggregateCommand.AggregateId); - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/src/Squidex.Infrastructure/DomainObjectVersionException.cs index 9a269cbbc..19599594d 100644 --- a/src/Squidex.Infrastructure/DomainObjectVersionException.cs +++ b/src/Squidex.Infrastructure/DomainObjectVersionException.cs @@ -12,20 +12,20 @@ namespace Squidex.Infrastructure { public class DomainObjectVersionException : DomainObjectException { - private readonly int currentVersion; - private readonly int expectedVersion; + private readonly long currentVersion; + private readonly long expectedVersion; - public int CurrentVersion + public long CurrentVersion { get { return currentVersion; } } - public int ExpectedVersion + public long ExpectedVersion { get { return expectedVersion; } } - public DomainObjectVersionException(string id, Type type, int currentVersion, int expectedVersion) + public DomainObjectVersionException(string id, Type type, long currentVersion, long expectedVersion) : base(FormatMessage(id, type, currentVersion, expectedVersion), id, type) { this.currentVersion = currentVersion; @@ -33,7 +33,7 @@ namespace Squidex.Infrastructure this.expectedVersion = expectedVersion; } - private static string FormatMessage(string id, Type type, int currentVersion, int expectedVersion) + private static string FormatMessage(string id, Type type, long currentVersion, long expectedVersion) { return $"Requested version {expectedVersion} for object '{id}' (type {type}), but found {currentVersion}."; } diff --git a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs index c0ad8286c..eb2e1c1e0 100644 --- a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs +++ b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs @@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.Tasks public static class TaskHelper { public static readonly Task Done = CreateDoneTask(); + public static readonly Task False = CreateResultTask(false); + public static readonly Task True = CreateResultTask(true); private static Task CreateDoneTask() { @@ -22,5 +24,14 @@ namespace Squidex.Infrastructure.Tasks return result.Task; } + + private static Task CreateResultTask(bool value) + { + var result = new TaskCompletionSource(); + + result.SetResult(value); + + return result.Task; + } } } \ No newline at end of file diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs index 6d280253c..8ee5515bf 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs @@ -25,6 +25,10 @@ namespace Squidex.Read.MongoDb.Apps [BsonElement] public string MasterLanguage { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public HashSet Languages { get; set; } = new HashSet(); diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs index 6d9d4dfcf..729356ef9 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs @@ -36,6 +36,10 @@ namespace Squidex.Read.MongoDb.Contents [BsonElement] public string Text { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } diff --git a/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs index b3e7dec12..7a2ff5a2f 100644 --- a/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Read.MongoDb.History { - public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, ITrackCreatedByEntity + public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, IEntityWithCreatedBy { [BsonRequired] [BsonElement] @@ -40,7 +40,7 @@ namespace Squidex.Read.MongoDb.History [BsonElement] public Dictionary Parameters { get; set; } - RefToken ITrackCreatedByEntity.CreatedBy + RefToken IEntityWithCreatedBy.CreatedBy { get { diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs index ddbced902..a76e2ae6e 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs @@ -33,6 +33,10 @@ namespace Squidex.Read.MongoDb.Schemas [BsonElement] public string Schema { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } diff --git a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs index 0e628b70e..bff43a0be 100644 --- a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs @@ -23,6 +23,7 @@ namespace Squidex.Read.MongoDb.Utils SetId(headers, entity); + SetVersion(headers, entity); SetCreated(headers, entity); SetCreatedBy(@event, entity); @@ -33,6 +34,7 @@ namespace Squidex.Read.MongoDb.Utils public static T Update(SquidexEvent @event, EnvelopeHeaders headers, T entity) where T : MongoEntity, new() { + SetVersion(headers, entity); SetLastModified(headers, entity); SetLastModifiedBy(@event, entity); @@ -54,23 +56,33 @@ namespace Squidex.Read.MongoDb.Utils entity.LastModified = headers.Timestamp(); } + private static void SetVersion(EnvelopeHeaders headers, MongoEntity entity) + { + var withVersion = entity as IEntityWithVersion; + + if (withVersion != null) + { + withVersion.Version = headers.EventNumber(); + } + } + private static void SetCreatedBy(SquidexEvent @event, MongoEntity entity) { - var createdBy = entity as ITrackCreatedByEntity; + var withCreatedBy = entity as IEntityWithCreatedBy; - if (createdBy != null) + if (withCreatedBy != null) { - createdBy.CreatedBy = @event.Actor; + withCreatedBy.CreatedBy = @event.Actor; } } private static void SetLastModifiedBy(SquidexEvent @event, MongoEntity entity) { - var modifiedBy = entity as ITrackLastModifiedByEntity; + var withModifiedBy = entity as IEntityWithLastModifiedBy; - if (modifiedBy != null) + if (withModifiedBy != null) { - modifiedBy.LastModifiedBy = @event.Actor; + withModifiedBy.LastModifiedBy = @event.Actor; } } diff --git a/src/Squidex.Read/Apps/IAppEntity.cs b/src/Squidex.Read/Apps/IAppEntity.cs index 79b7aa401..455aa00dd 100644 --- a/src/Squidex.Read/Apps/IAppEntity.cs +++ b/src/Squidex.Read/Apps/IAppEntity.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Read.Apps { - public interface IAppEntity : IEntity + public interface IAppEntity : IEntity, IEntityWithVersion { string Name { get; } diff --git a/src/Squidex.Read/Contents/IContentEntity.cs b/src/Squidex.Read/Contents/IContentEntity.cs index 78460b80e..f672b129c 100644 --- a/src/Squidex.Read/Contents/IContentEntity.cs +++ b/src/Squidex.Read/Contents/IContentEntity.cs @@ -10,7 +10,7 @@ using Squidex.Core.Contents; namespace Squidex.Read.Contents { - public interface IContentEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity + public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { bool IsPublished { get; } diff --git a/src/Squidex.Read/ITrackCreatedByEntity.cs b/src/Squidex.Read/IEntityWithCreatedBy.cs similarity index 91% rename from src/Squidex.Read/ITrackCreatedByEntity.cs rename to src/Squidex.Read/IEntityWithCreatedBy.cs index 6d79a3f7f..5aad9c2f9 100644 --- a/src/Squidex.Read/ITrackCreatedByEntity.cs +++ b/src/Squidex.Read/IEntityWithCreatedBy.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Read { - public interface ITrackCreatedByEntity + public interface IEntityWithCreatedBy { RefToken CreatedBy { get; set; } } diff --git a/src/Squidex.Read/ITrackLastModifiedByEntity.cs b/src/Squidex.Read/IEntityWithLastModifiedBy.cs similarity index 90% rename from src/Squidex.Read/ITrackLastModifiedByEntity.cs rename to src/Squidex.Read/IEntityWithLastModifiedBy.cs index d19b36266..11976ab9a 100644 --- a/src/Squidex.Read/ITrackLastModifiedByEntity.cs +++ b/src/Squidex.Read/IEntityWithLastModifiedBy.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Read { - public interface ITrackLastModifiedByEntity + public interface IEntityWithLastModifiedBy { RefToken LastModifiedBy { get; set; } } diff --git a/src/Squidex.Read/IEntityWithVersion.cs b/src/Squidex.Read/IEntityWithVersion.cs new file mode 100644 index 000000000..195e53bf2 --- /dev/null +++ b/src/Squidex.Read/IEntityWithVersion.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IEntityWithVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Read +{ + public interface IEntityWithVersion + { + long Version { get; set; } + } +} diff --git a/src/Squidex.Read/Schemas/ISchemaEntity.cs b/src/Squidex.Read/Schemas/ISchemaEntity.cs index 33c5479ce..5d01a6f54 100644 --- a/src/Squidex.Read/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Read/Schemas/ISchemaEntity.cs @@ -8,7 +8,7 @@ namespace Squidex.Read.Schemas { - public interface ISchemaEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity + public interface ISchemaEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { string Name { get; } diff --git a/src/Squidex.Write/Apps/AppCommandHandler.cs b/src/Squidex.Write/Apps/AppCommandHandler.cs index d77de95f9..ddaad7535 100644 --- a/src/Squidex.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Write/Apps/AppCommandHandler.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Apps.Repositories; using Squidex.Read.Users.Repositories; using Squidex.Write.Apps.Commands; @@ -51,10 +52,8 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot create a new app", error); } - await handler.CreateAsync(command, x => + await handler.CreateAsync(context, x => { - x.Create(command); - context.Succeed(command.AggregateId); }); } @@ -70,15 +69,15 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot assign contributor to app", error); } - await handler.UpdateAsync(command, x => + await handler.UpdateAsync(context, x => { - x.AssignContributor(command); + context.Succeed(new EntitySavedResult(x.Version)); }); } protected Task On(AttachClient command, CommandContext context) { - return handler.UpdateAsync(command, x => + return handler.UpdateAsync(context, x => { x.AttachClient(command, keyGenerator.GenerateKey()); @@ -88,37 +87,37 @@ namespace Squidex.Write.Apps protected Task On(RemoveContributor command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RemoveContributor(command)); + return handler.UpdateAsync(context, x => x.RemoveContributor(command)); } protected Task On(RenameClient command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RenameClient(command)); + return handler.UpdateAsync(context, x => x.RenameClient(command)); } protected Task On(RevokeClient command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RevokeClient(command)); + return handler.UpdateAsync(context, x => x.RevokeClient(command)); } protected Task On(AddLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.AddLanguage(command)); + return handler.UpdateAsync(context, x => x.AddLanguage(command)); } protected Task On(RemoveLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RemoveLanguage(command)); + return handler.UpdateAsync(context, x => x.RemoveLanguage(command)); } protected Task On(SetMasterLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.SetMasterLanguage(command)); + return handler.UpdateAsync(context, x => x.SetMasterLanguage(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } } } diff --git a/src/Squidex.Write/Contents/ContentCommandHandler.cs b/src/Squidex.Write/Contents/ContentCommandHandler.cs index e63785ca8..fe4376422 100644 --- a/src/Squidex.Write/Contents/ContentCommandHandler.cs +++ b/src/Squidex.Write/Contents/ContentCommandHandler.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Apps.Services; using Squidex.Read.Schemas.Services; using Squidex.Write.Contents.Commands; @@ -42,46 +43,41 @@ namespace Squidex.Write.Contents { await ValidateAsync(command, () => "Failed to create content"); - await handler.CreateAsync(command, s => - { - s.Create(command); - - context.Succeed(command.ContentId); - }); + await handler.CreateAsync(context, c => c.Create(command)); } protected async Task On(UpdateContent command, CommandContext context) { await ValidateAsync(command, () => "Failed to update content"); - await handler.UpdateAsync(command, s => s.Update(command)); + await handler.UpdateAsync(context, c => c.Update(command)); } protected async Task On(PatchContent command, CommandContext context) { await ValidateAsync(command, () => "Failed to patch content"); - await handler.UpdateAsync(command, s => s.Patch(command)); + await handler.UpdateAsync(context, c => c.Patch(command)); } protected Task On(PublishContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Publish(command)); + return handler.UpdateAsync(context, c => c.Publish(command)); } protected Task On(UnpublishContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Unpublish(command)); + return handler.UpdateAsync(context, c => c.Unpublish(command)); } protected Task On(DeleteContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Delete(command)); + return handler.UpdateAsync(context, c => c.Delete(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } private async Task ValidateAsync(ContentDataCommand command, Func message) diff --git a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs index 2e8af2d13..7bcd2cc34 100644 --- a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs +++ b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Schemas.Services; using Squidex.Write.Schemas.Commands; @@ -41,77 +42,72 @@ namespace Squidex.Write.Schemas throw new ValidationException("Cannot create a new schema", error); } - await handler.CreateAsync(command, s => - { - s.Create(command); - - context.Succeed(command.Name); - }); + await handler.CreateAsync(context, s => s.Create(command)); } protected Task On(AddField command, CommandContext context) { - return handler.UpdateAsync(command, s => + return handler.UpdateAsync(context, s => { s.AddField(command); - context.Succeed(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id); + context.Succeed(new EntityCreatedResult(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id, s.Version)); }); } protected Task On(DeleteSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Delete(command)); + return handler.UpdateAsync(context, s => s.Delete(command)); } protected Task On(DeleteField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.DeleteField(command)); + return handler.UpdateAsync(context, s => s.DeleteField(command)); } protected Task On(DisableField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.DisableField(command)); + return handler.UpdateAsync(context, s => s.DisableField(command)); } protected Task On(EnableField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.EnableField(command)); + return handler.UpdateAsync(context, s => s.EnableField(command)); } protected Task On(HideField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.HideField(command)); + return handler.UpdateAsync(context, s => s.HideField(command)); } protected Task On(ShowField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.ShowField(command)); + return handler.UpdateAsync(context, s => s.ShowField(command)); } protected Task On(UpdateSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Update(command)); + return handler.UpdateAsync(context, s => s.Update(command)); } protected Task On(UpdateField command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.UpdateField(command); }); + return handler.UpdateAsync(context, s => s.UpdateField(command)); } protected Task On(PublishSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.Publish(command); }); + return handler.UpdateAsync(context, s => s.Publish(command)); } protected Task On(UnpublishSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.Unpublish(command); }); + return handler.UpdateAsync(context, s => s.Unpublish(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } } } diff --git a/src/Squidex.Write/SquidexCommand.cs b/src/Squidex.Write/SquidexCommand.cs index ae3637166..aed4baa52 100644 --- a/src/Squidex.Write/SquidexCommand.cs +++ b/src/Squidex.Write/SquidexCommand.cs @@ -14,5 +14,7 @@ namespace Squidex.Write public abstract class SquidexCommand : ICommand { public RefToken Actor { get; set; } + + public long? ExpectedVersion { get; set; } } } diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index 767a48e86..37206e9cc 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -10,7 +10,6 @@ using Autofac; using Microsoft.Extensions.Configuration; using Squidex.Core.Schemas; using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.CQRS.Events; using Squidex.Pipeline.CommandHandlers; using Squidex.Write.Apps; using Squidex.Write.Contents; @@ -29,6 +28,10 @@ namespace Squidex.Config.Domain protected override void Load(ContainerBuilder builder) { + builder.RegisterType() + .As() + .SingleInstance(); + builder.RegisterType() .As() .SingleInstance(); @@ -45,10 +48,6 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() - .As() - .SingleInstance(); - builder.RegisterType() .AsSelf() .SingleInstance(); @@ -69,6 +68,10 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); + builder.Register>(c => (id => new AppDomainObject(id, 0))) .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Identity/IdentityUsage.cs b/src/Squidex/Config/Identity/IdentityUsage.cs index c83eb7a51..1fef70487 100644 --- a/src/Squidex/Config/Identity/IdentityUsage.cs +++ b/src/Squidex/Config/Identity/IdentityUsage.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using Squidex.Core.Identity; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -140,7 +141,7 @@ namespace Squidex.Config.Identity { context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); - return Task.FromResult(true); + return TaskHelper.Done; } public override async Task CreatingTicket(OAuthCreatingTicketContext context) diff --git a/src/Squidex/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Config/Swagger/XmlTagProcessor.cs index b104971fb..19847d1bb 100644 --- a/src/Squidex/Config/Swagger/XmlTagProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlTagProcessor.cs @@ -12,6 +12,7 @@ using NJsonSchema.Infrastructure; using NSwag.Annotations; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -57,7 +58,7 @@ namespace Squidex.Config.Swagger context.OperationDescription.Operation.Tags.Add(tagAttribute.Name); } - return Task.FromResult(true); + return TaskHelper.True; } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs index fd5237a82..90807b329 100644 --- a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Controllers.Api.Apps.Models; using Squidex.Core.Identity; @@ -64,6 +65,8 @@ namespace Squidex.Controllers.Api.Apps var response = entity.Clients.Select(x => SimpleMapper.Map(x, new ClientDto())).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(response); } @@ -82,15 +85,15 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPost] [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientDto[]), 201)] + [ProducesResponseType(typeof(ClientDto), 201)] public async Task PostClient(string app, [FromBody] CreateAppClientDto request) { var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient())); - var result = context.Result(); + var result = context.Result>().IdOrValue; var response = SimpleMapper.Map(result, new ClientDto()); - return StatusCode(201, response); + return CreatedAtAction(nameof(GetClients), new { app }, response); } /// @@ -105,7 +108,6 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPut] [Route("apps/{app}/clients/{clientId}/")] - [ProducesResponseType(typeof(ClientDto[]), 201)] public async Task PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request) { await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId })); diff --git a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs index d94e47244..bd9133b3e 100644 --- a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -60,6 +61,8 @@ namespace Squidex.Controllers.Api.Apps var response = entity.Contributors.Select(x => SimpleMapper.Map(x, new ContributorDto())).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(response); } diff --git a/src/Squidex/Controllers/Api/Apps/AppController.cs b/src/Squidex/Controllers/Api/Apps/AppController.cs index e34195ab2..4e6a90b51 100644 --- a/src/Squidex/Controllers/Api/Apps/AppController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppController.cs @@ -92,9 +92,11 @@ namespace Squidex.Controllers.Api.Apps var command = SimpleMapper.Map(request, new CreateApp()); var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return CreatedAtAction(nameof(GetApps), new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>(); + var response = new EntityCreatedDto { Id = result.ToString(), Version = result.Version }; + + return CreatedAtAction(nameof(GetApps), response); } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs index 3dcc070b9..b1f37a5da 100644 --- a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -66,6 +67,8 @@ namespace Squidex.Controllers.Api.Apps return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage }); }).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } @@ -89,7 +92,7 @@ namespace Squidex.Controllers.Api.Apps var response = SimpleMapper.Map(request.Language, new AppLanguageDto()); - return StatusCode(201, response); + return CreatedAtAction(nameof(GetLanguages), new { app }, response); } /// diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs index 2321855ea..9e6a4b929 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs @@ -24,6 +24,11 @@ namespace Squidex.Controllers.Api.Apps.Models [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] public string Name { get; set; } + /// + /// The version of the app. + /// + public long Version { get; set; } + /// /// The name of the app. /// diff --git a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs b/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs index 63ac305e0..86ca96ae2 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs @@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Controllers.Api.Apps.Models { - public sealed class ClientDto + public class ClientDto { /// /// The client id. diff --git a/src/Squidex/Controllers/Api/EntityCreatedDto.cs b/src/Squidex/Controllers/Api/EntityCreatedDto.cs index fad9e6c60..b60d8dbb4 100644 --- a/src/Squidex/Controllers/Api/EntityCreatedDto.cs +++ b/src/Squidex/Controllers/Api/EntityCreatedDto.cs @@ -10,12 +10,17 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Controllers.Api { - public class EntityCreatedDto + public sealed class EntityCreatedDto { /// /// Id of the created entity. /// [Required] public string Id { get; set; } + + /// + /// The new version of the entity. + /// + public long Version { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs index 47b9d56ba..88d0ee4a6 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs @@ -54,9 +54,11 @@ namespace Squidex.Controllers.Api.Schemas var command = new AddField { Name = request.Name, Properties = request.Properties.ToProperties() }; var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return StatusCode(201, new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>().IdOrValue; + var response = new EntityCreatedDto { Id = result.ToString() }; + + return StatusCode(201, response); } /// diff --git a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs index 25bfbfd9f..e6b9cd0f9 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -80,6 +81,8 @@ namespace Squidex.Controllers.Api.Schemas var model = entity.ToModel(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 4599cc4ae..a69b4fcbe 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using Squidex.Controllers.Api; using Squidex.Controllers.ContentApi.Models; using Squidex.Core.Contents; @@ -91,20 +92,22 @@ namespace Squidex.Controllers.ContentApi return NotFound(); } - var content = await contentRepository.FindContentAsync(schemaEntity.Id, id); + var entity = await contentRepository.FindContentAsync(schemaEntity.Id, id); - if (content == null) + if (entity == null) { return NotFound(); } - var model = SimpleMapper.Map(content, new ContentDto()); + var model = SimpleMapper.Map(entity, new ContentDto()); - if (content.Data != null) + if (entity.Data != null) { - model.Data = content.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); + model.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); } + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } @@ -115,9 +118,11 @@ namespace Squidex.Controllers.ContentApi var command = new CreateContent { Data = request, ContentId = Guid.NewGuid() }; var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return CreatedAtAction(nameof(GetContent), new { id = result }, new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>().IdOrValue; + var response = new EntityCreatedDto { Id = result.ToString() }; + + return CreatedAtAction(nameof(GetContent), new { id = result }, response); } [HttpPut] diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 140bb0b8d..10bd02d85 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -80,6 +80,7 @@ namespace Squidex.Controllers.ContentApi.Generator GenerateSecurityDefinitions(); GenerateSecurityRequirements(); GenerateDefaultErrors(); + GeneratePing(); return document; } @@ -108,7 +109,7 @@ namespace Squidex.Controllers.ContentApi.Generator { ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } }, - Title = $"Suidex API for {app.Name} App", + Title = $"Suidex API for {app.Name} App" }; } @@ -206,6 +207,24 @@ namespace Squidex.Controllers.ContentApi.Generator } } + private void GeneratePing() + { + var swaggerOperation = AddOperation(SwaggerOperationMethod.Get, null, $"ping/{app.Name}", operation => + { + operation.OperationId = "MakePingTest"; + + operation.Description = "Make a simple request, e.g. to test credentials."; + + operation.Summary = "Make Test"; + + }); + + foreach (var operation in swaggerOperation.Values) + { + operation.Tags = new List { "PingTest" }; + } + } + private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation => diff --git a/src/Squidex/Controllers/ContentApi/PingController.cs b/src/Squidex/Controllers/ContentApi/PingController.cs new file mode 100644 index 000000000..cc07b3fb8 --- /dev/null +++ b/src/Squidex/Controllers/ContentApi/PingController.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// PingController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Squidex.Core.Identity; +using Squidex.Pipeline; + +namespace Squidex.Controllers.ContentApi +{ + [Authorize(Roles = SquidexRoles.AppEditor)] + [ApiExceptionFilter] + [ServiceFilter(typeof(AppFilterAttribute))] + public class PingController : ControllerBase + { + [HttpGet] + [Route("ping/{app}/")] + public IActionResult GetPing() + { + return Ok(); + } + } +} diff --git a/src/Squidex/Controllers/UI/Account/AccountController.cs b/src/Squidex/Controllers/UI/Account/AccountController.cs index 3e160cb73..b701907d1 100644 --- a/src/Squidex/Controllers/UI/Account/AccountController.cs +++ b/src/Squidex/Controllers/UI/Account/AccountController.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Config.Identity; using Squidex.Core.Identity; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf // ReSharper disable RedundantIfElseBlock @@ -216,7 +217,7 @@ namespace Squidex.Controllers.UI.Account { if (isFirst || !identityOptions.Value.LockAutomatically) { - return Task.FromResult(true); + return TaskHelper.True; } return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); @@ -226,7 +227,7 @@ namespace Squidex.Controllers.UI.Account { if (!isFirst) { - return Task.FromResult(true); + return TaskHelper.True; } return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator)); diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs index 9ceb6d18e..ed0fc424c 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Tasks; using Squidex.Write; // ReSharper disable InvertIf @@ -37,15 +38,10 @@ namespace Squidex.Pipeline.CommandHandlers FindActorFromSubject() ?? FindActorFromClient(); - if (actorToken == null) - { - throw new SecurityException("No actor with subject or client id available"); - } - - squidexCommand.Actor = actorToken; + squidexCommand.Actor = actorToken ?? throw new SecurityException("No actor with subject or client id available"); } - return Task.FromResult(false); + return TaskHelper.False; } private RefToken FindActorFromSubject() diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs index 30482bd9f..993ce1e7e 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; using Squidex.Write; // ReSharper disable InvertIf @@ -42,7 +43,7 @@ namespace Squidex.Pipeline.CommandHandlers appCommand.AppId = new NamedId(appFeature.App.Id, appFeature.App.Name); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs new file mode 100644 index 000000000..c6637360e --- /dev/null +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// EnrichWithExpectedVersionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Pipeline.CommandHandlers +{ + public sealed class EnrichWithExpectedVersionHandler : ICommandHandler + { + private readonly IHttpContextAccessor httpContextAccessor; + + public EnrichWithExpectedVersionHandler(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task HandleAsync(CommandContext context) + { + var headers = httpContextAccessor.HttpContext.Request.GetTypedHeaders(); + var headerMatch = headers.IfMatch?.FirstOrDefault(); + + if (!string.IsNullOrWhiteSpace(headerMatch?.Tag) && long.TryParse(headerMatch.Tag, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion)) + { + context.Command.ExpectedVersion = expectedVersion; + } + + return TaskHelper.False; + } + } +} diff --git a/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs b/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs new file mode 100644 index 000000000..37e18f5fb --- /dev/null +++ b/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// SetVersionAsETagHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Pipeline.CommandHandlers +{ + public class SetVersionAsETagHandler : ICommandHandler + { + private readonly IHttpContextAccessor httpContextAccessor; + + public SetVersionAsETagHandler(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task HandleAsync(CommandContext context) + { + var result = context.Result() as EntitySavedResult; + + if (result != null) + { + httpContextAccessor.HttpContext.Response.Headers["ETag"] = new StringValues(result.Version.ToString()); + } + + return TaskHelper.False; + } + } +} diff --git a/src/Squidex/Views/Shared/Docs.cshtml b/src/Squidex/Views/Shared/Docs.cshtml index 48ce58060..e0c66898d 100644 --- a/src/Squidex/Views/Shared/Docs.cshtml +++ b/src/Squidex/Views/Shared/Docs.cshtml @@ -26,7 +26,7 @@ - + diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs index c51c8490c..9c4b627af 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs @@ -25,11 +25,13 @@ namespace Squidex.Infrastructure.CQRS.Commands private sealed class MyCommand : IAggregateCommand { public Guid AggregateId { get; set; } + + public long? ExpectedVersion { get; set; } } private sealed class MyDomainObject : DomainObject { - public MyDomainObject(Guid id, int version) + public MyDomainObject(Guid id, int version) : base(id, version) { } @@ -48,19 +50,16 @@ namespace Squidex.Infrastructure.CQRS.Commands private readonly Mock factory = new Mock(); private readonly Mock repository = new Mock(); - private readonly Mock processor1 = new Mock(); - private readonly Mock processor2 = new Mock(); private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); + private readonly CommandContext context; private readonly MyCommand command; private readonly AggregateHandler sut; private readonly MyDomainObject domainObject; public AggregateHandlerTests() { - var processors = new[] { processor1.Object, processor2.Object }; - - sut = new AggregateHandler(factory.Object, repository.Object, processors); + sut = new AggregateHandler(factory.Object, repository.Object); domainObject = new MyDomainObject(Guid.NewGuid(), 1) @@ -68,6 +67,7 @@ namespace Squidex.Infrastructure.CQRS.Commands .RaiseNewEvent(event2); command = new MyCommand { AggregateId = domainObject.Id }; + context = new CommandContext(command); } [Fact] @@ -89,21 +89,23 @@ namespace Squidex.Infrastructure.CQRS.Commands .Returns(domainObject) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.CreateAsync(command, x => - { - passedDomainObject = x; + MyDomainObject passedDomainObject = null; - return TaskHelper.Done; - }); + await sut.CreateAsync(context, async x => + { + await Task.Delay(1); - Assert.Equal(domainObject, passedDomainObject); + passedDomainObject = x; }); - factory.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + repository.VerifyAll(); } [Fact] @@ -113,95 +115,69 @@ namespace Squidex.Infrastructure.CQRS.Commands .Returns(domainObject) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.CreateAsync(command, x => - { - passedDomainObject = x; - }); + MyDomainObject passedDomainObject = null; - Assert.Equal(domainObject, passedDomainObject); + await sut.CreateAsync(context, x => + { + passedDomainObject = x; }); - factory.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + repository.VerifyAll(); } [Fact] public async Task Update_async_should_create_domain_object_and_save() { - repository.Setup(x => x.GetByIdAsync(command.AggregateId, int.MaxValue)) + repository.Setup(x => x.GetByIdAsync(command.AggregateId, null)) .Returns(Task.FromResult(domainObject)) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.UpdateAsync(command, x => - { - passedDomainObject = x; + MyDomainObject passedDomainObject = null; - return TaskHelper.Done; - }); + await sut.UpdateAsync(context, async x => + { + await Task.Delay(1); - Assert.Equal(domainObject, passedDomainObject); + passedDomainObject = x; }); + + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); + + repository.VerifyAll(); } [Fact] public async Task Update_sync_should_create_domain_object_and_save() { - repository.Setup(x => x.GetByIdAsync(command.AggregateId, int.MaxValue)) + repository.Setup(x => x.GetByIdAsync(command.AggregateId, null)) .Returns(Task.FromResult(domainObject)) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; - - await sut.UpdateAsync(command, x => - { - passedDomainObject = x; - }); - - Assert.Equal(domainObject, passedDomainObject); - }); - } - - private async Task TestFlowAsync(Func action) - { - repository.Setup(x => x.SaveAsync(domainObject, - It.IsAny>>(), - It.IsAny())) - .Returns(TaskHelper.Done) - .Verifiable(); - - processor1.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event1.Payload), domainObject, command)) + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) .Returns(TaskHelper.Done) .Verifiable(); - processor2.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event1.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); - - processor1.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event2.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); + MyDomainObject passedDomainObject = null; - processor2.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event2.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); - - await action(); + await sut.UpdateAsync(context, x => + { + passedDomainObject = x; + }); - processor1.VerifyAll(); - processor2.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); repository.VerifyAll(); } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs index 189894e93..f98ca3d70 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using Moq; using System; using Xunit; @@ -13,11 +14,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { public class CommandContextTests { - private readonly MyCommand command = new MyCommand(); - - private sealed class MyCommand : ICommand - { - } + private readonly ICommand command = new Mock().Object; [Fact] public void Should_instantiate_and_provide_command() diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs index 997a3050c..850366649 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs @@ -14,6 +14,7 @@ using Squidex.Infrastructure.CQRS.Events; using Xunit; using System.Collections.Generic; using System.Linq; +using Squidex.Infrastructure.Tasks; // ReSharper disable ImplicitlyCapturedClosure // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable @@ -141,7 +142,8 @@ namespace Squidex.Infrastructure.CQRS.Commands eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event2), commitId)).Returns(eventData2); eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, It.Is>(e => e.Count() == 2))) - .Returns(Task.FromResult(true)).Verifiable(); + .Returns(TaskHelper.Done) + .Verifiable(); domainObject.AddEvent(event1); domainObject.AddEvent(event2); @@ -166,7 +168,8 @@ namespace Squidex.Infrastructure.CQRS.Commands eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event2), commitId)).Returns(eventData2); eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, new List { eventData1, eventData2 })) - .Throws(new WrongEventVersionException(1, 2)).Verifiable(); + .Throws(new WrongEventVersionException(1, 2)) + .Verifiable(); domainObject.AddEvent(event1); domainObject.AddEvent(event2); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs index 4140b90f8..c24c679d4 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs @@ -15,13 +15,11 @@ namespace Squidex.Infrastructure.CQRS.Commands { public sealed class EnrichWithTimestampHandlerTests { - private sealed class MyNormalCommand : ICommand - { - } - private sealed class MyTimestampCommand : ITimestampCommand { public Instant Timestamp { get; set; } + + public long? ExpectedVersion { get; set; } } private readonly Mock clock = new Mock(); @@ -47,7 +45,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { var sut = new EnrichWithTimestampHandler(clock.Object); - var result = await sut.HandleAsync(new CommandContext(new MyNormalCommand())); + var result = await sut.HandleAsync(new CommandContext(new Mock().Object)); Assert.False(result); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs index 408a4b423..e56468030 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs @@ -8,18 +8,16 @@ using System; using System.Threading.Tasks; +using Moq; +using Squidex.Infrastructure.Tasks; using Xunit; namespace Squidex.Infrastructure.CQRS.Commands { public class InMemoryCommandBusTests { - private readonly MyCommand command = new MyCommand(); - - private sealed class MyCommand : ICommand - { - } - + private readonly ICommand command = new Mock().Object; + private sealed class HandledHandler : ICommandHandler { public ICommand LastCommand; @@ -28,7 +26,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastCommand = context.Command; - return Task.FromResult(true); + return TaskHelper.True; } } @@ -40,7 +38,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastCommand = context.Command; - return Task.FromResult(false); + return TaskHelper.False; } } @@ -64,7 +62,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastException = context.Exception; - return Task.FromResult(false); + return TaskHelper.False; } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs index 60f3da8c0..e2290acb2 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Xunit; using System.Collections.Generic; using System.Linq; +using Moq; namespace Squidex.Infrastructure.CQRS.Commands { @@ -19,6 +20,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly MyLogger logger = new MyLogger(); private readonly LogExceptionHandler sut; + private readonly ICommand command = new Mock().Object; private sealed class MyLogger : ILogger { @@ -40,10 +42,6 @@ namespace Squidex.Infrastructure.CQRS.Commands } } - private sealed class MyCommand : ICommand - { - } - public LogExceptionHandlerTests() { sut = new LogExceptionHandler(logger); @@ -52,7 +50,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_do_nothing_if_command_is_succeeded() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); context.Succeed(); @@ -65,7 +63,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_if_command_failed() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); context.Fail(new InvalidOperationException()); @@ -78,8 +76,8 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_if_command_is_not_handled() { - var context = new CommandContext(new MyCommand()); - + var context = new CommandContext(command); + var isHandled = await sut.HandleAsync(context); Assert.False(isHandled); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs index c09e6a353..cbf04c40b 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Moq; using Xunit; namespace Squidex.Infrastructure.CQRS.Commands @@ -17,6 +18,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly MyLogger logger = new MyLogger(); private readonly LogExecutingHandler sut; + private readonly ICommand command = new Mock().Object; private sealed class MyLogger : ILogger { @@ -37,11 +39,7 @@ namespace Squidex.Infrastructure.CQRS.Commands return null; } } - - private sealed class MyCommand : ICommand - { - } - + public LogExecutingHandlerTests() { sut = new LogExecutingHandler(logger); @@ -50,7 +48,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_once() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); var isHandled = await sut.HandleAsync(context); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs deleted file mode 100644 index f75c712a3..000000000 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// EnrichWithAggregateIdProcessorTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; -using Xunit; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public class EnrichWithAggregateIdProcessorTests - { - public sealed class MyAggregateIdCommand : IAggregateCommand - { - public Guid AggregateId { get; set; } - } - - public sealed class MyNormalCommand : ICommand - { - } - - public sealed class MyEvent : IEvent - { - } - - private readonly EnrichWithAggregateIdProcessor sut = new EnrichWithAggregateIdProcessor(); - - [Fact] - public async Task Should_not_do_anything_if_not_aggregate_command() - { - var envelope = new Envelope(new MyEvent()); - - await sut.ProcessEventAsync(envelope, null, new MyNormalCommand()); - - Assert.False(envelope.Headers.Contains("AggregateId")); - } - - [Fact] - public async Task Should_attach_aggregate_to_event_envelope() - { - var aggregateId = Guid.NewGuid(); - var aggregateCommand = new MyAggregateIdCommand { AggregateId = aggregateId }; - - var envelope = new Envelope(new MyEvent()); - - await sut.ProcessEventAsync(envelope, null, aggregateCommand); - - Assert.Equal(aggregateId, envelope.Headers.AggregateId()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs index 4848058b0..e05616428 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs @@ -12,6 +12,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; +using Squidex.Infrastructure.Tasks; using Xunit; // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -134,7 +135,7 @@ namespace Squidex.Infrastructure.CQRS.Events { consumerInfo.LastHandledEventNumber = 2L; - eventConsumer.Setup(x => x.On(envelope1)).Returns(Task.FromResult(true)); + eventConsumer.Setup(x => x.On(envelope1)).Returns(TaskHelper.True); eventConsumer.Setup(x => x.On(envelope2)).Throws(new InvalidOperationException()); sut.Subscribe(eventConsumer.Object); diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index aed05cd5b..2d36e6f38 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -48,7 +48,9 @@ namespace Squidex.Write.Apps { var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); - appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(AppName)) + .Returns(Task.FromResult(new Mock().Object)) + .Verifiable(); await TestCreate(app, async _ => { @@ -63,7 +65,9 @@ namespace Squidex.Write.Apps { var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); - appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult(null)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(AppName)) + .Returns(Task.FromResult(null)) + .Verifiable(); await TestCreate(app, async _ => { @@ -135,7 +139,9 @@ namespace Squidex.Write.Apps [Fact] public async Task AttachClient_should_update_domain_object() { - keyGenerator.Setup(x => x.GenerateKey()).Returns(clientSecret).Verifiable(); + keyGenerator.Setup(x => x.GenerateKey()) + .Returns(clientSecret) + .Verifiable(); CreateApp(); diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index b1d977fa2..68ebd1251 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -40,7 +40,9 @@ namespace Squidex.Write.Schemas { var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)) + .Returns(Task.FromResult(new Mock().Object)) + .Verifiable(); await TestCreate(schema, async _ => { @@ -55,7 +57,9 @@ namespace Squidex.Write.Schemas { var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult(null)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)) + .Returns(Task.FromResult(null)) + .Verifiable(); await TestCreate(schema, async _ => { diff --git a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs index c594f85e2..ecb1fdfbd 100644 --- a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs @@ -32,14 +32,14 @@ namespace Squidex.Write.TestHelpers IsUpdated = false; } - public Task CreateAsync(IAggregateCommand command, Func creator) where V : class, IAggregate + public Task CreateAsync(CommandContext context, Func creator) where V : class, IAggregate { IsCreated = true; return creator(domainObject as V); } - public Task UpdateAsync(IAggregateCommand command, Func updater) where V : class, IAggregate + public Task UpdateAsync(CommandContext context, Func updater) where V : class, IAggregate { IsUpdated = true; From 1c182ae47f76c38b7e555c5f709656764bd7052c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 3 Mar 2017 23:57:22 +0100 Subject: [PATCH 18/66] Performance improvements --- src/Squidex.Core/Schemas/Field.cs | 2 +- src/Squidex.Core/Schemas/StringFieldEditor.cs | 2 +- .../Json/ConverterContractResolver.cs | 44 ++++++++++ .../Contents/MongoContentRepository.cs | 35 ++++---- .../MongoContentRepository_EventHandling.cs | 8 +- .../Contents/Visitors/EdmModelExtensions.cs | 27 ++++++ .../Contents/Builders/EdmModelBuilder.cs} | 47 +++++++---- src/Squidex.Read/Squidex.Read.csproj | 5 ++ src/Squidex/Config/Domain/ReadModule.cs | 5 ++ src/Squidex/Config/Domain/Serializers.cs | 20 ++--- .../Pipeline/ApiExceptionFilterAttribute.cs | 6 ++ .../Schemas/DateTimeFieldPropertiesTests.cs | 2 +- .../Schemas/DateTimeFieldTests.cs | 14 +++- .../Schemas/JsonFieldTests.cs | 6 +- .../Squidex.Core.Tests/Schemas/SchemaTests.cs | 19 +++-- .../Schemas/SchemaValidationTests.cs | 10 +-- .../CQRS/Commands/AggregateHandlerTests.cs | 12 +++ .../Json/ConverterContractResolverTests.cs | 84 +++++++++++++++++++ .../Json/InstantConverterTests.cs | 26 ++++++ .../TestHelpers/JsonHelper.cs | 21 ++++- .../Apps/CachingAppProviderTests.cs | 26 ++++-- .../MongoDb/Contents/ODataQueryTests.cs | 34 ++++++-- .../Schemas/CachingSchemaProviderTests.cs | 28 +++++-- .../Contents/ContentCommandHandlerTests.cs | 2 - .../Schemas/SchemaCommandHandlerTests.cs | 5 +- .../Squidex.Write.Tests.csproj | 4 - 26 files changed, 391 insertions(+), 103 deletions(-) create mode 100644 src/Squidex.Infrastructure/Json/ConverterContractResolver.cs create mode 100644 src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs rename src/{Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs => Squidex.Read/Contents/Builders/EdmModelBuilder.cs} (57%) create mode 100644 tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 967d79848..3a48de351 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -86,7 +86,7 @@ namespace Squidex.Core.Schemas var rawErrors = new List(); try { - var typedValue = value.Type == JTokenType.Null ? null : ConvertValue(value); + var typedValue = value.Type == JTokenType.Null ? null : ConvertValue(value); foreach (var validator in validators.Value) { diff --git a/src/Squidex.Core/Schemas/StringFieldEditor.cs b/src/Squidex.Core/Schemas/StringFieldEditor.cs index fb92c7f71..6a222e401 100644 --- a/src/Squidex.Core/Schemas/StringFieldEditor.cs +++ b/src/Squidex.Core/Schemas/StringFieldEditor.cs @@ -10,9 +10,9 @@ namespace Squidex.Core.Schemas { public enum StringFieldEditor { - Dropdown, Input, Markdown, + Dropdown, Radio, RichText, TextArea diff --git a/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs b/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs new file mode 100644 index 000000000..625d36fa1 --- /dev/null +++ b/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// ConverterContractResolver.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Squidex.Infrastructure.Json +{ + public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver + { + private readonly JsonConverter[] converters; + + public ConverterContractResolver(params JsonConverter[] converters) + { + this.converters = converters; + } + + protected override JsonConverter ResolveContractConverter(Type objectType) + { + var result = base.ResolveContractConverter(objectType); + + if (result != null) + { + return result; + } + + foreach (var converter in converters) + { + if (converter.CanConvert(objectType)) + { + return converter; + } + } + + return null; + } + } +} diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs index a29daffa5..2791c07ba 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs @@ -12,12 +12,13 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.OData.Core; using MongoDB.Driver; -using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.Contents; +using Squidex.Read.Contents.Builders; using Squidex.Read.Contents.Repositories; using Squidex.Read.MongoDb.Contents.Visitors; +using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; namespace Squidex.Read.MongoDb.Contents @@ -27,6 +28,7 @@ namespace Squidex.Read.MongoDb.Contents private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; private readonly ISchemaProvider schemaProvider; + private readonly EdmModelBuilder modelBuilder; protected static IndexKeysDefinitionBuilder IndexKeys { @@ -36,13 +38,14 @@ namespace Squidex.Read.MongoDb.Contents } } - public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemaProvider) + public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemaProvider, EdmModelBuilder modelBuilder) { Guard.NotNull(database, nameof(database)); + Guard.NotNull(modelBuilder, nameof(modelBuilder)); Guard.NotNull(schemaProvider, nameof(schemaProvider)); this.database = database; - + this.modelBuilder = modelBuilder; this.schemaProvider = schemaProvider; } @@ -50,14 +53,16 @@ namespace Squidex.Read.MongoDb.Contents { List result = null; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { IFindFluent cursor; try { - var parser = schema.ParseQuery(languages, odataQuery); + var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + + var parser = model.ParseQuery(odataQuery); - cursor = collection.Find(parser, schema, nonPublished).Take(parser).Skip(parser).Sort(parser, schema); + cursor = collection.Find(parser, schemaEntity.Schema, nonPublished).Take(parser).Skip(parser).Sort(parser, schemaEntity.Schema); } catch (NotSupportedException) { @@ -76,7 +81,7 @@ namespace Squidex.Read.MongoDb.Contents foreach (var entity in entities) { - entity.ParseData(schema); + entity.ParseData(schemaEntity.Schema); } result = entities.OfType().ToList(); @@ -89,14 +94,16 @@ namespace Squidex.Read.MongoDb.Contents { var result = 0L; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { IFindFluent cursor; try { - var parser = schema.ParseQuery(languages, odataQuery); + var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + + var parser = model.ParseQuery(odataQuery); - cursor = collection.Find(parser, schema, nonPublished); + cursor = collection.Find(parser, schemaEntity.Schema, nonPublished); } catch (NotSupportedException) { @@ -121,17 +128,17 @@ namespace Squidex.Read.MongoDb.Contents { MongoContentEntity result = null; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { result = await collection.Find(x => x.Id == id).FirstOrDefaultAsync(); - result?.ParseData(schema); + result?.ParseData(schemaEntity.Schema); }); return result; } - private async Task ForSchemaAsync(Guid schemaId, Func, Schema, Task> action) + private async Task ForSchemaAsync(Guid schemaId, Func, ISchemaEntityWithSchema, Task> action) { var collection = GetCollection(schemaId); @@ -142,7 +149,7 @@ namespace Squidex.Read.MongoDb.Contents return; } - await action(collection, schemaEntity.Schema); + await action(collection, schemaEntity); } } } diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 8574dd475..e20978138 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -68,24 +68,24 @@ namespace Squidex.Read.MongoDb.Contents protected Task On(ContentCreated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.SchemaId.Id, (collection, schemaEntity) => { return collection.CreateAsync(@event, headers, x => { SimpleMapper.Map(@event, x); - x.SetData(schema, @event.Data); + x.SetData(schemaEntity.Schema, @event.Data); }); }); } protected Task On(ContentUpdated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.SchemaId.Id, (collection, schemaEntity) => { return collection.UpdateAsync(@event, headers, x => { - x.SetData(schema, @event.Data); + x.SetData(schemaEntity.Schema, @event.Data); }); }); } diff --git a/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs b/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs new file mode 100644 index 000000000..d3b992635 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// SchemaExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Core.UriParser; +using Microsoft.OData.Edm; + +namespace Squidex.Read.MongoDb.Contents.Visitors +{ + public static class EdmModelExtensions + { + public static ODataUriParser ParseQuery(this IEdmModel model, string query) + { + var path = model.EntityContainer.EntitySets().First().Path.Path.Last().Split('.').Last(); + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + } +} diff --git a/src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs similarity index 57% rename from src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs rename to src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs index 7a9cf44ca..3dda0acb2 100644 --- a/src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs +++ b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs @@ -1,5 +1,5 @@ // ========================================================================== -// SchemaExtensions.cs +// EdmModelBuilder.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,19 +8,45 @@ using System; using System.Collections.Generic; -using Microsoft.OData.Core.UriParser; +using System.Linq; +using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Read.Schemas; +using Squidex.Read.Utils; -namespace Squidex.Read.MongoDb.Contents.Visitors +namespace Squidex.Read.Contents.Builders { - public static class SchemaExtensions + public sealed class EdmModelBuilder : CachingProvider { - public static EdmModel BuildEdmModel(this Schema schema, HashSet languages) + public EdmModelBuilder(IMemoryCache cache) + : base(cache) + { + } + + public IEdmModel BuildEdmModel(ISchemaEntityWithSchema schemaEntity, HashSet languages) + { + Guard.NotNull(languages, nameof(languages)); + Guard.NotNull(schemaEntity, nameof(schemaEntity)); + + var cacheKey = $"{schemaEntity.Id}_{schemaEntity.Version}_{string.Join(",", languages.Select(x => x.Iso2Code).OrderBy(x => x))}"; + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); + + return BuildEdmModel(schemaEntity.Schema, languages); + }); + + return result; + } + + private static EdmModel BuildEdmModel(Schema schema, HashSet languages) { var model = new EdmModel(); + var container = new EdmEntityContainer("Squidex", "Container"); var schemaType = schema.BuildEdmType(languages, x => @@ -41,18 +67,9 @@ namespace Squidex.Read.MongoDb.Contents.Visitors model.AddElement(schemaType); model.AddElement(entityType); - container.AddEntitySet($"{schema.Name}_Set", entityType); + container.AddEntitySet("ContentSet", entityType); return model; } - - public static ODataUriParser ParseQuery(this Schema schema, HashSet languages, string query) - { - var model = schema.BuildEdmModel(languages); - - var parser = new ODataUriParser(model, new Uri($"{schema.Name}_Set?{query}", UriKind.Relative)); - - return parser; - } } } diff --git a/src/Squidex.Read/Squidex.Read.csproj b/src/Squidex.Read/Squidex.Read.csproj index 911f23a17..af62c3bc9 100644 --- a/src/Squidex.Read/Squidex.Read.csproj +++ b/src/Squidex.Read/Squidex.Read.csproj @@ -16,4 +16,9 @@ + + + C:\Users\mail2\.nuget\packages\identityserver4\1.1.1\lib\netstandard1.4\IdentityServer4.dll + + diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index 980b63975..cc7613ed3 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -12,6 +12,7 @@ using Squidex.Read.Apps; using Squidex.Read.Apps.Services; using Squidex.Read.Apps.Services.Implementations; using Squidex.Read.Contents; +using Squidex.Read.Contents.Builders; using Squidex.Read.History; using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; @@ -49,6 +50,10 @@ namespace Squidex.Config.Domain builder.RegisterType() .As() .SingleInstance(); + + builder.RegisterType() + .AsSelf() + .SingleInstance(); } } } diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 3f23d2323..507af12f8 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -10,7 +10,6 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; using NodaTime; using NodaTime.Serialization.JsonNet; using Squidex.Events; @@ -27,16 +26,15 @@ namespace Squidex.Config.Domain { settings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - - settings.Converters.Add(new InstantConverter()); - settings.Converters.Add(new LanguageConverter()); - settings.Converters.Add(new NamedGuidIdConverter()); - settings.Converters.Add(new NamedLongIdConverter()); - settings.Converters.Add(new NamedStringIdConverter()); - settings.Converters.Add(new PropertiesBagConverter()); - settings.Converters.Add(new RefTokenConverter()); - settings.Converters.Add(new StringEnumConverter()); + settings.ContractResolver = new ConverterContractResolver( + new InstantConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertiesBagConverter(), + new RefTokenConverter(), + new StringEnumConverter()); settings.NullValueHandling = NullValueHandling.Ignore; diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index 5a9b7db4c..2283c2c7f 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -35,6 +35,7 @@ namespace Squidex.Pipeline static ApiExceptionFilterAttribute() { AddHandler(OnDomainObjectNotFoundException); + AddHandler(OnDomainObjectVersionException); AddHandler(OnDomainException); AddHandler(OnValidationException); } @@ -44,6 +45,11 @@ namespace Squidex.Pipeline return new NotFoundResult(); } + private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) + { + return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 409 }; + } + private static IActionResult OnDomainException(DomainException ex) { return new BadRequestObjectResult(new ErrorDto { Message = ex.Message }); diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs index 067b08a35..365b65bb5 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs @@ -136,7 +136,7 @@ namespace Squidex.Core.Schemas private static Instant FutureDays(int days) { - return SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(days)); + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); } } } \ No newline at end of file diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs index 9a251e461..117a40be7 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; @@ -89,9 +90,20 @@ namespace Squidex.Core.Schemas new[] { "My-DateTime is not a valid value" }); } + [Fact] + public async Task Should_add_errors_if_value_is_another_type() + { + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { Label = "My-DateTime" }); + + await sut.ValidateAsync(CreateValue(123), errors); + + errors.ShouldBeEquivalentTo( + new[] { "My-DateTime is not a valid value" }); + } + private static Instant FutureDays(int days) { - return SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(days)); + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); } private static JValue CreateValue(object v) diff --git a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs index 0076e4a8a..8815bfc76 100644 --- a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs @@ -39,17 +39,17 @@ namespace Squidex.Core.Schemas { var sut = new JsonField(1, "my-json", new JsonFieldProperties { Label = "My-Json" }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(new JValue(1)), errors); Assert.Empty(errors); } [Fact] - public async Task Should_add_errors_if_datetime_is_required() + public async Task Should_add_errors_if_json_is_required() { var sut = new JsonField(1, "my-json", new JsonFieldProperties { Label = "My-Json", IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); errors.ShouldBeEquivalentTo( new[] { "My-Json is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs index c12608625..cdb9fc223 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using Newtonsoft.Json.Linq; +using NJsonSchema; using Squidex.Infrastructure; using Xunit; @@ -255,9 +256,9 @@ namespace Squidex.Core.Schemas { var languages = new HashSet(new[] { Language.DE, Language.EN }); - var json = BuildMixedSchema().BuildSchema(languages, (n, s) => s).ToJson(); + var jsonSchema = BuildMixedSchema().BuildSchema(languages, (n, s) => new JsonSchema4 { SchemaReference = s }); - Assert.NotNull(json); + Assert.NotNull(jsonSchema); } [Fact] @@ -276,19 +277,19 @@ namespace Squidex.Core.Schemas var schema = Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddOrUpdateField(new JsonField(0, "my-json", + .AddOrUpdateField(new JsonField(1, "my-json", new JsonFieldProperties())) - .AddOrUpdateField(new StringField(1, "my-string1", + .AddOrUpdateField(new StringField(2, "my-string1", new StringFieldProperties { Label = "My String1", IsLocalizable = true, IsRequired = true, AllowedValues = allowedValues })) - .AddOrUpdateField(new StringField(2, "my-string2", + .AddOrUpdateField(new StringField(3, "my-string2", new StringFieldProperties { Hints = "My String1" })) - .AddOrUpdateField(new NumberField(3, "my-number", + .AddOrUpdateField(new NumberField(4, "my-number", new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddOrUpdateField(new BooleanField(4, "my-boolean", + .AddOrUpdateField(new BooleanField(5, "my-boolean", new BooleanFieldProperties())) - .AddOrUpdateField(new DateTimeField(5, "my-datetime", + .AddOrUpdateField(new DateTimeField(6, "my-datetime", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) - .AddOrUpdateField(new DateTimeField(6, "my-date", + .AddOrUpdateField(new DateTimeField(7, "my-date", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })); return schema; diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs index 8999e19a2..b94c2c3d0 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs @@ -293,15 +293,15 @@ namespace Squidex.Core.Schemas var schema = Schema.Create("my-schema", new SchemaProperties()) - .AddOrUpdateField(new JsonField(0, "my-json", + .AddOrUpdateField(new JsonField(1, "my-json", new JsonFieldProperties())) - .AddOrUpdateField(new StringField(1, "my-string", + .AddOrUpdateField(new StringField(2, "my-string", new StringFieldProperties { DefaultValue = "EN-String", IsLocalizable = true })) - .AddOrUpdateField(new NumberField(2, "my-number", + .AddOrUpdateField(new NumberField(3, "my-number", new NumberFieldProperties { DefaultValue = 123 })) - .AddOrUpdateField(new BooleanField(3, "my-boolean", + .AddOrUpdateField(new BooleanField(4, "my-boolean", new BooleanFieldProperties { DefaultValue = true })) - .AddOrUpdateField(new DateTimeField(4, "my-datetime", + .AddOrUpdateField(new DateTimeField(5, "my-datetime", new DateTimeFieldProperties { DefaultValue = now })); var data = diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs index 9c4b627af..a1278a0b7 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs @@ -82,6 +82,12 @@ namespace Squidex.Infrastructure.CQRS.Commands Assert.Equal(repository.Object, sut.Repository); } + [Fact] + public Task Create_async_should_throw_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.CreateAsync(new CommandContext(new Mock().Object), x => TaskHelper.False)); + } + [Fact] public async Task Create_async_should_create_domain_object_and_save() { @@ -132,6 +138,12 @@ namespace Squidex.Infrastructure.CQRS.Commands repository.VerifyAll(); } + [Fact] + public Task Update_async_should_throw_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(new CommandContext(new Mock().Object), x => TaskHelper.False)); + } + [Fact] public async Task Update_async_should_create_domain_object_and_save() { diff --git a/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs b/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs new file mode 100644 index 000000000..b19bae215 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// ConverterContractResolverTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using NodaTime; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public class ConverterContractResolverTests + { + public class MyClass + { + [JsonConverter(typeof(TodayConverter))] + public Instant MyProperty { get; set; } + } + + public sealed class TodayConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue("TODAY"); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Instant); + } + } + + [Fact] + public void Should_respect_property_converter() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = new ConverterContractResolver(new InstantConverter()) + }; + + var json = JsonConvert.SerializeObject(new MyClass { MyProperty = value }, serializerSettings); + + Assert.Equal(@"{ ""MyProperty"": ""TODAY"" }", json); + } + + [Fact] + public void Should_ignore_other_converters() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = new ConverterContractResolver(new InstantConverter()) + }; + + serializerSettings.Converters.Add(new TodayConverter()); + + var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); + var output = JsonConvert.DeserializeObject>(result, serializerSettings); + + Assert.Equal(value, output.Item1); + } + + [Fact] + public void Should_serialize_and_deserialize_instant() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + value.SerializeAndDeserialize(new ConverterContractResolver(new InstantConverter())); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs new file mode 100644 index 000000000..59d9492be --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// InstantConverterTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public sealed class InstantConverterTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + value.SerializeAndDeserialize(new InstantConverter()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs index a1f0799be..d1585a0dc 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs @@ -8,13 +8,28 @@ using System; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using Xunit; namespace Squidex.Infrastructure.TestHelpers { public static class JsonHelper { - public static void SerializeAndDeserialize(this T value, JsonConverter converter) where T : class + public static void SerializeAndDeserialize(this T value, IContractResolver contractResolver) + { + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = contractResolver, + NullValueHandling = NullValueHandling.Include + }; + + var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); + var output = JsonConvert.DeserializeObject>(result, serializerSettings); + + Assert.Equal(value, output.Item1); + } + + public static void SerializeAndDeserialize(this T value, JsonConverter converter) { var serializerSettings = new JsonSerializerSettings(); @@ -27,14 +42,14 @@ namespace Squidex.Infrastructure.TestHelpers Assert.Equal(value, output.Item1); } - public static void DoesNotDeserialize(string value, JsonConverter converter) where T : class + public static void DoesNotDeserialize(string value, JsonConverter converter) { var serializerSettings = new JsonSerializerSettings(); serializerSettings.Converters.Add(converter); serializerSettings.NullValueHandling = NullValueHandling.Include; - Assert.Throws(() => JsonConvert.DeserializeObject>($"{{ \"Item1\": \"{value}\" }}")); + Assert.ThrowsAny(() => JsonConvert.DeserializeObject>($"{{ \"Item1\": \"{value}\" }}", serializerSettings)); } } } diff --git a/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs b/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs index 5f4429110..a3f80b572 100644 --- a/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs +++ b/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs @@ -14,7 +14,6 @@ using Moq; using Squidex.Infrastructure; using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Services.Implementations; -using Squidex.Read.MongoDb.Apps; using Xunit; // ReSharper disable ConvertToConstant.Local @@ -27,14 +26,23 @@ namespace Squidex.Read.Apps private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly Mock repository = new Mock(); private readonly CachingAppProvider sut; - private readonly MongoAppEntity appV1; - private readonly MongoAppEntity appV2; + private readonly IAppEntity appV1; + private readonly IAppEntity appV2; private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); public CachingAppProviderTests() { - appV1 = new MongoAppEntity { Name = appId.Name, Id = appId.Id }; - appV2 = new MongoAppEntity { Name = appId.Name, Id = appId.Id }; + var appV1Mock = new Mock(); + var appV2Mock = new Mock(); + + appV1Mock.Setup(x => x.Id).Returns(appId.Id); + appV1Mock.Setup(x => x.Name).Returns(appId.Name); + + appV2Mock.Setup(x => x.Id).Returns(appId.Id); + appV2Mock.Setup(x => x.Name).Returns(appId.Name); + + appV1 = appV1Mock.Object; + appV2 = appV2Mock.Object; sut = new CachingAppProvider(cache, repository.Object); } @@ -42,7 +50,7 @@ namespace Squidex.Read.Apps [Fact] public async Task Should_also_retrieve_app_by_name_if_retrieved_by_id_before() { - repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(Task.FromResult(appV1)); + repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(Task.FromResult(appV1)); await ProvideAppById(appV1); await ProvideAppByName(appV1); @@ -54,7 +62,7 @@ namespace Squidex.Read.Apps [Fact] public async Task Should_also_retrieve_app_by_id_if_retrieved_by_name_before() { - repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(Task.FromResult(appV1)); + repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(Task.FromResult(appV1)); await ProvideAppByName(appV1); await ProvideAppById(appV1); @@ -68,7 +76,7 @@ namespace Squidex.Read.Apps { var apps = ProviderResults(appV1, appV2); - repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(() => Task.FromResult(apps())); + repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(() => Task.FromResult(apps())); await ProvideAppById(appV1); @@ -84,7 +92,7 @@ namespace Squidex.Read.Apps { var apps = ProviderResults(appV1, appV2); - repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(() => Task.FromResult(apps())); + repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(() => Task.FromResult(apps())); await ProvideAppByName(appV1); diff --git a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs index 882ea67b9..dca834f67 100644 --- a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs @@ -9,13 +9,18 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OData.Edm; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Moq; using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; +using Squidex.Read.Contents.Builders; using Squidex.Read.MongoDb.Contents.Visitors; +using Squidex.Read.Schemas; using Xunit; // ReSharper disable SpecifyACultureInStringConversionExplicitly @@ -39,6 +44,7 @@ namespace Squidex.Read.MongoDb.Contents private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + private readonly IEdmModel edmModel; private readonly HashSet languages = new HashSet { Language.EN, @@ -50,10 +56,22 @@ namespace Squidex.Read.MongoDb.Contents InstantSerializer.Register(); } + public ODataQueryTests() + { + var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); + + var schemaEntity = new Mock(); + schemaEntity.Setup(x => x.Id).Returns(Guid.NewGuid()); + schemaEntity.Setup(x => x.Version).Returns(3); + schemaEntity.Setup(x => x.Schema).Returns(schema); + + edmModel = builder.BuildEdmModel(schemaEntity.Object, languages); + } + [Fact] public void Should_parse_query() { - var parser = schema.ParseQuery(languages, "$filter=data/firstName/de eq 'Sebastian'"); + var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); Assert.NotNull(parser); } @@ -223,7 +241,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_top() { - var parser = schema.ParseQuery(languages, "$top=3"); + var parser = edmModel.ParseQuery("$top=3"); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -234,7 +252,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_max_top_if_larger() { - var parser = schema.ParseQuery(languages, "$top=300"); + var parser = edmModel.ParseQuery("$top=300"); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -245,7 +263,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_default_top() { - var parser = schema.ParseQuery(languages, ""); + var parser = edmModel.ParseQuery(""); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -256,7 +274,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_skip() { - var parser = schema.ParseQuery(languages, "$skip=3"); + var parser = edmModel.ParseQuery("$skip=3"); var cursor = new Mock>(); cursor.Object.Skip(parser); @@ -267,7 +285,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_not_set_skip() { - var parser = schema.ParseQuery(languages, ""); + var parser = edmModel.ParseQuery(""); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -282,7 +300,7 @@ namespace Squidex.Read.MongoDb.Contents private string S(string value) { - var parser = schema.ParseQuery(languages, value); + var parser = edmModel.ParseQuery(value); var cursor = new Mock>(); var i = string.Empty; @@ -299,7 +317,7 @@ namespace Squidex.Read.MongoDb.Contents private string F(string value) { - var parser = schema.ParseQuery(languages, value); + var parser = edmModel.ParseQuery(value); var query = FilterBuilder.Build(parser, schema).Render(serializer, registry).ToString(); diff --git a/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs b/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs index 8c1ddb489..a86cb4db7 100644 --- a/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs +++ b/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs @@ -14,7 +14,6 @@ using Moq; using Squidex.Infrastructure; using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Services.Implementations; -using Squidex.Read.MongoDb.Schemas; using Xunit; // ReSharper disable ConvertToConstant.Local @@ -27,15 +26,26 @@ namespace Squidex.Read.Schemas private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly Mock repository = new Mock(); private readonly CachingSchemaProvider sut; - private readonly MongoSchemaEntity schemaV1; - private readonly MongoSchemaEntity schemaV2; + private readonly ISchemaEntityWithSchema schemaV1; + private readonly ISchemaEntityWithSchema schemaV2; private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); public CachingSchemaProviderTests() { - schemaV1 = new MongoSchemaEntity { Name = schemaId.Name, Id = schemaId.Id, AppId = appId.Id }; - schemaV2 = new MongoSchemaEntity { Name = schemaId.Name, Id = schemaId.Id, AppId = appId.Id }; + var schemaV1Mock = new Mock(); + var schemaV2Mock = new Mock(); + + schemaV1Mock.Setup(x => x.Id).Returns(schemaId.Id); + schemaV1Mock.Setup(x => x.Name).Returns(schemaId.Name); + schemaV1Mock.Setup(x => x.AppId).Returns(appId.Id); + + schemaV2Mock.Setup(x => x.Id).Returns(schemaId.Id); + schemaV2Mock.Setup(x => x.Name).Returns(schemaId.Name); + schemaV2Mock.Setup(x => x.AppId).Returns(appId.Id); + + schemaV1 = schemaV1Mock.Object; + schemaV2 = schemaV2Mock.Object; sut = new CachingSchemaProvider(cache, repository.Object); } @@ -43,7 +53,7 @@ namespace Squidex.Read.Schemas [Fact] public async Task Should_also_retrieve_schema_by_name_if_retrieved_by_id_before() { - repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(Task.FromResult(schemaV1)); + repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(Task.FromResult(schemaV1)); await ProvideSchemaById(schemaV1); await ProvideSchemaByName(schemaV1); @@ -55,7 +65,7 @@ namespace Squidex.Read.Schemas [Fact] public async Task Should_also_retrieve_schema_by_id_if_retrieved_by_name_before() { - repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(Task.FromResult(schemaV1)); + repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(Task.FromResult(schemaV1)); await ProvideSchemaByName(schemaV1); await ProvideSchemaById(schemaV1); @@ -69,7 +79,7 @@ namespace Squidex.Read.Schemas { var schemas = ProviderResults(schemaV1, schemaV2); - repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(() => Task.FromResult(schemas())); + repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(() => Task.FromResult(schemas())); await ProvideSchemaById(schemaV1); @@ -85,7 +95,7 @@ namespace Squidex.Read.Schemas { var schemas = ProviderResults(schemaV1, schemaV2); - repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(() => Task.FromResult(schemas())); + repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(() => Task.FromResult(schemas())); await ProvideSchemaByName(schemaV1); diff --git a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs index 9f329362f..681471797 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -73,8 +73,6 @@ namespace Squidex.Write.Contents { await sut.HandleAsync(context); }); - - Assert.Equal(contentId, context.Result()); } [Fact] diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index 68ebd1251..bc9a93699 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Moq; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; using Squidex.Write.Schemas.Commands; @@ -65,8 +66,6 @@ namespace Squidex.Write.Schemas { await sut.HandleAsync(context); }); - - Assert.Equal(SchemaName, context.Result()); } [Fact] @@ -134,7 +133,7 @@ namespace Squidex.Write.Schemas await sut.HandleAsync(context); }); - Assert.Equal(1, context.Result()); + Assert.Equal(1, context.Result>().IdOrValue); } [Fact] diff --git a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj index b9ef1380c..acad1fcf1 100644 --- a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj +++ b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj @@ -5,10 +5,6 @@ $(PackageTargetFallback);dnxcore50 Squidex.Write - - full - True - From ee3857bd819c786a132e93bb11d5dfb52f3cb3f9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 4 Mar 2017 19:18:23 +0100 Subject: [PATCH 19/66] Optimisic concurrency --- .../Commands/DefaultDomainObjectFactory.cs | 9 +- .../Commands/DefaultDomainObjectRepository.cs | 3 +- .../CQRS/DomainObject.cs | 2 +- .../Contents/MongoContentRepository.cs | 10 +- src/Squidex.Write/Apps/AppCommandHandler.cs | 12 +-- .../Contents/ContentCommandHandler.cs | 11 ++- src/Squidex/Config/Domain/WriteModule.cs | 6 +- .../Api/Languages/LanguagesController.cs | 2 +- .../Api/Schemas/Models/SchemaDetailsDto.cs | 5 + .../Api/Schemas/Models/SchemaDto.cs | 5 + .../ContentApi/ContentsController.cs | 11 ++- .../ContentApi/Models/ContentDto.cs | 5 + .../Pipeline/ApiExceptionFilterAttribute.cs | 2 +- .../EnrichWithExpectedVersionHandler.cs | 6 +- .../EnrichWithSchemaIdHandler.cs | 8 +- src/Squidex/Views/Shared/Docs.cshtml | 2 +- .../pages/content/content-page.component.ts | 12 ++- .../pages/contents/contents-page.component.ts | 31 ++++--- .../app/features/content/pages/messages.ts | 6 +- .../app/features/schemas/pages/messages.ts | 3 +- .../schema/schema-edit-form.component.ts | 8 +- .../pages/schema/schema-page.component.html | 1 + .../pages/schema/schema-page.component.ts | 27 +++--- .../pages/schemas/schema-form.component.ts | 16 ++-- .../pages/schemas/schemas-page.component.ts | 6 +- .../pages/clients/client.component.html | 2 +- .../pages/clients/clients-page.component.ts | 13 ++- .../contributors-page.component.ts | 13 ++- .../languages/languages-page.component.ts | 17 ++-- .../app/framework/angular/http-utils.ts | 9 +- src/Squidex/app/framework/declarations.ts | 3 +- src/Squidex/app/framework/utils/version.ts | 21 +++++ .../resolve-app-languages.guard.spec.ts | 6 +- .../guards/resolve-app-languages.guard.ts | 2 +- .../guards/resolve-content.guard.spec.ts | 6 +- .../shared/guards/resolve-content.guard.ts | 2 +- .../resolve-published-schema.guard.spec.ts | 8 +- .../guards/resolve-published-schema.guard.ts | 2 +- .../guards/resolve-schema.guard.spec.ts | 6 +- .../app/shared/guards/resolve-schema.guard.ts | 2 +- .../services/app-clients.service.spec.ts | 20 ++-- .../shared/services/app-clients.service.ts | 18 ++-- .../services/app-contributors.service.spec.ts | 16 ++-- .../services/app-contributors.service.ts | 14 +-- .../services/app-languages.service.spec.ts | 20 ++-- .../shared/services/app-languages.service.ts | 18 ++-- .../app/shared/services/auth.service.ts | 67 ++++++++------ .../shared/services/contents.service.spec.ts | 49 ++++++---- .../app/shared/services/contents.service.ts | 36 ++++---- .../shared/services/schemas.service.spec.ts | 91 +++++++++++-------- .../app/shared/services/schemas.service.ts | 57 ++++++------ src/Squidex/app/theme/_bootstrap.scss | 4 +- 52 files changed, 434 insertions(+), 297 deletions(-) create mode 100644 src/Squidex/app/framework/utils/version.ts diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs index 24efb3956..1308de330 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs @@ -28,7 +28,14 @@ namespace Squidex.Infrastructure.CQRS.Commands var factoryFunctionType = typeof(DomainObjectFactoryFunction<>).MakeGenericType(type); var factoryFunction = (Delegate)serviceProvider.GetService(factoryFunctionType); - return (IAggregate)factoryFunction.DynamicInvoke(id); + var aggregate = (IAggregate)factoryFunction.DynamicInvoke(id); + + if (aggregate.Version != -1) + { + throw new InvalidOperationException("Must have a version of -1"); + } + + return aggregate; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs index 517fa6583..3c163beb2 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs @@ -74,8 +74,7 @@ namespace Squidex.Infrastructure.CQRS.Commands var streamName = nameResolver.GetStreamName(domainObject.GetType(), domainObject.Id); var versionCurrent = domainObject.Version; - var versionBefore = versionCurrent - events.Count; - var versionExpected = versionBefore == 0 ? -1 : versionBefore - 1; + var versionExpected = versionCurrent - events.Count; var eventsToSave = events.Select(x => formatter.ToEventData(x, commitId)).ToList(); diff --git a/src/Squidex.Infrastructure/CQRS/DomainObject.cs b/src/Squidex.Infrastructure/CQRS/DomainObject.cs index b9130fedf..14cfd3be5 100644 --- a/src/Squidex.Infrastructure/CQRS/DomainObject.cs +++ b/src/Squidex.Infrastructure/CQRS/DomainObject.cs @@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.CQRS protected DomainObject(Guid id, int version) { Guard.NotEmpty(id, nameof(id)); - Guard.GreaterEquals(version, 0, nameof(version)); + Guard.GreaterEquals(version, -1, nameof(version)); this.id = id; diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs index 2791c07ba..341fb3695 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs @@ -27,7 +27,7 @@ namespace Squidex.Read.MongoDb.Contents { private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; private readonly EdmModelBuilder modelBuilder; protected static IndexKeysDefinitionBuilder IndexKeys @@ -38,15 +38,15 @@ namespace Squidex.Read.MongoDb.Contents } } - public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemaProvider, EdmModelBuilder modelBuilder) + public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas, EdmModelBuilder modelBuilder) { Guard.NotNull(database, nameof(database)); Guard.NotNull(modelBuilder, nameof(modelBuilder)); - Guard.NotNull(schemaProvider, nameof(schemaProvider)); + Guard.NotNull(schemas, nameof(schemas)); + this.schemas = schemas; this.database = database; this.modelBuilder = modelBuilder; - this.schemaProvider = schemaProvider; } public async Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages) @@ -142,7 +142,7 @@ namespace Squidex.Read.MongoDb.Contents { var collection = GetCollection(schemaId); - var schemaEntity = await schemaProvider.FindSchemaByIdAsync(schemaId); + var schemaEntity = await schemas.FindSchemaByIdAsync(schemaId); if (schemaEntity == null) { diff --git a/src/Squidex.Write/Apps/AppCommandHandler.cs b/src/Squidex.Write/Apps/AppCommandHandler.cs index ddaad7535..99d46fa58 100644 --- a/src/Squidex.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Write/Apps/AppCommandHandler.cs @@ -52,10 +52,7 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot create a new app", error); } - await handler.CreateAsync(context, x => - { - context.Succeed(command.AggregateId); - }); + await handler.CreateAsync(context, x => x.Create(command)); } protected async Task On(AssignContributor command, CommandContext context) @@ -69,10 +66,7 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot assign contributor to app", error); } - await handler.UpdateAsync(context, x => - { - context.Succeed(new EntitySavedResult(x.Version)); - }); + await handler.UpdateAsync(context, x => x.AssignContributor(command)); } protected Task On(AttachClient command, CommandContext context) @@ -81,7 +75,7 @@ namespace Squidex.Write.Apps { x.AttachClient(command, keyGenerator.GenerateKey()); - context.Succeed(x.Clients[command.Id]); + context.Succeed(new EntityCreatedResult(x.Clients[command.Id], x.Version)); }); } diff --git a/src/Squidex.Write/Contents/ContentCommandHandler.cs b/src/Squidex.Write/Contents/ContentCommandHandler.cs index fe4376422..c0f4f1b9d 100644 --- a/src/Squidex.Write/Contents/ContentCommandHandler.cs +++ b/src/Squidex.Write/Contents/ContentCommandHandler.cs @@ -23,20 +23,21 @@ namespace Squidex.Write.Contents { private readonly IAggregateHandler handler; private readonly IAppProvider appProvider; - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; public ContentCommandHandler( IAggregateHandler handler, IAppProvider appProvider, - ISchemaProvider schemaProvider) + ISchemaProvider schemas) { Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(schemas, nameof(schemas)); Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(schemaProvider, nameof(schemaProvider)); this.handler = handler; + this.schemas = schemas; + this.appProvider = appProvider; - this.schemaProvider = schemaProvider; } protected async Task On(CreateContent command, CommandContext context) @@ -88,7 +89,7 @@ namespace Squidex.Write.Contents appProvider.FindAppByIdAsync(command.AppId.Id); var taskForSchema = - schemaProvider.FindSchemaByIdAsync(command.SchemaId.Id); + schemas.FindSchemaByIdAsync(command.SchemaId.Id); await Task.WhenAll(taskForApp, taskForSchema); diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index 37206e9cc..bf3211f88 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -72,11 +72,11 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.Register>(c => (id => new AppDomainObject(id, 0))) + builder.Register>(c => (id => new AppDomainObject(id, -1))) .AsSelf() .SingleInstance(); - builder.Register>(c => (id => new ContentDomainObject(id, 0))) + builder.Register>(c => (id => new ContentDomainObject(id, -1))) .AsSelf() .SingleInstance(); @@ -84,7 +84,7 @@ namespace Squidex.Config.Domain { var fieldRegistry = c.Resolve(); - return (id => new SchemaDomainObject(id, 0, fieldRegistry)); + return (id => new SchemaDomainObject(id, -1, fieldRegistry)); }) .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs b/src/Squidex/Controllers/Api/Languages/LanguagesController.cs index bed7d44b1..8f6ab922e 100644 --- a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs +++ b/src/Squidex/Controllers/Api/Languages/LanguagesController.cs @@ -38,7 +38,7 @@ namespace Squidex.Controllers.Api.Languages [ProducesResponseType(typeof(string[]), 200)] public IActionResult GetLanguages() { - var response = Language.AllLanguages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList(); + var response = Language.AllLanguages.Where(x => x != Language.Invariant).Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList(); return Ok(response); } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs index 6636348e9..6d0e1b28c 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs @@ -72,5 +72,10 @@ namespace Squidex.Controllers.Api.Schemas.Models /// The date and time when the schema has been modified last. /// public Instant LastModified { get; set; } + + /// + /// The version of the schema. + /// + public int Version { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs index aa1e50b75..f01171462 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs @@ -59,5 +59,10 @@ namespace Squidex.Controllers.Api.Schemas.Models /// The date and time when the schema has been modified last. /// public Instant LastModified { get; set; } + + /// + /// The version of the schema. + /// + public int Version { get; set; } } } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index a69b4fcbe..4f6cf483f 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -32,13 +32,14 @@ namespace Squidex.Controllers.ContentApi [ServiceFilter(typeof(AppFilterAttribute))] public class ContentsController : ControllerBase { - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; private readonly IContentRepository contentRepository; - public ContentsController(ICommandBus commandBus, ISchemaProvider schemaProvider, IContentRepository contentRepository) + public ContentsController(ICommandBus commandBus, ISchemaProvider schemas, IContentRepository contentRepository) : base(commandBus) { - this.schemaProvider = schemaProvider; + this.schemas = schemas; + this.contentRepository = contentRepository; } @@ -46,7 +47,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}")] public async Task GetContents(string name, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false) { - var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name); + var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name); if (schemaEntity == null) { @@ -85,7 +86,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/{id}")] public async Task GetContent(string name, Guid id, bool hidden = false) { - var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name); + var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name); if (schemaEntity == null) { diff --git a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs index f1e2693f6..ae09e993d 100644 --- a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs +++ b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs @@ -52,5 +52,10 @@ namespace Squidex.Controllers.ContentApi.Models /// Indicates if the content element is publihed. /// public bool IsPublished { get; set; } + + /// + /// The version of the content. + /// + public int Version { get; set; } } } diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index 2283c2c7f..a65042ac5 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -47,7 +47,7 @@ namespace Squidex.Pipeline private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) { - return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 409 }; + return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 412 }; } private static IActionResult OnDomainException(DomainException ex) diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs index c6637360e..764d932cb 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs @@ -26,10 +26,10 @@ namespace Squidex.Pipeline.CommandHandlers public Task HandleAsync(CommandContext context) { - var headers = httpContextAccessor.HttpContext.Request.GetTypedHeaders(); - var headerMatch = headers.IfMatch?.FirstOrDefault(); + var headers = httpContextAccessor.HttpContext.Request.Headers; + var headerMatch = headers["If-Match"].ToString(); - if (!string.IsNullOrWhiteSpace(headerMatch?.Tag) && long.TryParse(headerMatch.Tag, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion)) + if (!string.IsNullOrWhiteSpace(headerMatch) && long.TryParse(headerMatch, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion)) { context.Command.ExpectedVersion = expectedVersion; } diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs index a600573f7..5ca3f4c8a 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs @@ -21,12 +21,12 @@ namespace Squidex.Pipeline.CommandHandlers { public sealed class EnrichWithSchemaIdHandler : ICommandHandler { - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; private readonly IActionContextAccessor actionContextAccessor; - public EnrichWithSchemaIdHandler(ISchemaProvider schemaProvider, IActionContextAccessor actionContextAccessor) + public EnrichWithSchemaIdHandler(ISchemaProvider schemas, IActionContextAccessor actionContextAccessor) { - this.schemaProvider = schemaProvider; + this.schemas = schemas; this.actionContextAccessor = actionContextAccessor; } @@ -43,7 +43,7 @@ namespace Squidex.Pipeline.CommandHandlers { var schemaName = routeValues["name"].ToString(); - var schema = await schemaProvider.FindSchemaByNameAsync(schemaCommand.AppId.Id, schemaName); + var schema = await schemas.FindSchemaByNameAsync(schemaCommand.AppId.Id, schemaName); if (schema == null) { diff --git a/src/Squidex/Views/Shared/Docs.cshtml b/src/Squidex/Views/Shared/Docs.cshtml index e0c66898d..8006b6776 100644 --- a/src/Squidex/Views/Shared/Docs.cshtml +++ b/src/Squidex/Views/Shared/Docs.cshtml @@ -28,6 +28,6 @@ - + \ No newline at end of file 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 bec6f4169..be9fb02cd 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 @@ -28,7 +28,8 @@ import { SchemaDetailsDto, StringFieldPropertiesDto, UsersProviderService, - ValidatorsEx + ValidatorsEx, + Version } from 'shared'; @Component({ @@ -38,6 +39,7 @@ import { }) export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit { private messageSubscription: Subscription; + private version: Version; public schema: SchemaDetailsDto; @@ -94,9 +96,9 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, if (this.isNewMode) { this.appName() - .switchMap(app => this.contentsService.postContent(app, this.schema.name, data)) + .switchMap(app => this.contentsService.postContent(app, this.schema.name, data, this.version)) .subscribe(created => { - this.messageBus.publish(new ContentCreated(created.id, data)); + this.messageBus.publish(new ContentCreated(created.id, data, this.version.value)); this.router.navigate(['../'], { relativeTo: this.route }); }, error => { @@ -105,9 +107,9 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, }); } else { this.appName() - .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId, data)) + .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId, data, this.version)) .subscribe(() => { - this.messageBus.publish(new ContentUpdated(this.contentId, data)); + this.messageBus.publish(new ContentUpdated(this.contentId, 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 6b305dea8..7f9a92386 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 @@ -29,7 +29,8 @@ import { MessageBus, NotificationService, SchemaDetailsDto, - UsersProviderService + UsersProviderService, + Version } from 'shared'; @Component({ @@ -86,12 +87,12 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.messageBus.of(ContentCreated).subscribe(message => { this.itemLast++; this.contentTotal++; - this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data)); + this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data, message.version)); }); this.messageUpdatedSubscription = this.messageBus.of(ContentUpdated).subscribe(message => { - this.updateContents(message.id, undefined, message.data); + this.updateContents(message.id, undefined, message.data, message.version); }); this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => { @@ -115,9 +116,9 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public publishContent(content: ContentDto) { this.appName() - .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id)) + .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { - this.updateContents(content.id, true, content.data); + this.updateContents(content.id, true, content.data, content.version.value); }, error => { this.notifyError(error); }); @@ -125,9 +126,9 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public unpublishContent(content: ContentDto) { this.appName() - .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id)) + .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { - this.updateContents(content.id, false, content.data); + this.updateContents(content.id, false, content.data, content.version.value); }, error => { this.notifyError(error); }); @@ -135,7 +136,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public deleteContent(content: ContentDto) { this.appName() - .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id)) + .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { this.contentItems = this.contentItems.removeAll(x => x.id === content.id); @@ -204,11 +205,11 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.canGoPrev = this.currentPage > 0; } - private updateContents(id: string, p: boolean | undefined, data: any) { - this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data)); + private updateContents(id: string, p: boolean | undefined, data: any, version: string) { + this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data, version)); } - private createContent(id: string, data: any): ContentDto { + private createContent(id: string, data: any, version: string): ContentDto { const me = `subject:${this.authService.user!.id}`; const newContent = @@ -217,12 +218,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy me, me, DateTime.now(), DateTime.now(), - data); + data, + new Version(version)); return newContent; } - private updateContent(content: ContentDto, isPublished: boolean, data: any): ContentDto { + private updateContent(content: ContentDto, isPublished: boolean, data: any, version: string): ContentDto { const me = `subject:${this.authService.user!.id}`; const newContent = @@ -230,7 +232,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy content.id, isPublished, content.createdBy, me, content.created, DateTime.now(), - data); + data, + new Version(version)); return newContent; } diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 801c08dd3..93c69a917 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -8,7 +8,8 @@ export class ContentCreated { constructor( public readonly id: string, - public readonly data: any + public readonly data: any, + public readonly version: string ) { } } @@ -16,7 +17,8 @@ export class ContentCreated { export class ContentUpdated { constructor( public readonly id: string, - public readonly data: any + public readonly data: any, + public readonly version: string ) { } } diff --git a/src/Squidex/app/features/schemas/pages/messages.ts b/src/Squidex/app/features/schemas/pages/messages.ts index 28f2287cb..03ded7d36 100644 --- a/src/Squidex/app/features/schemas/pages/messages.ts +++ b/src/Squidex/app/features/schemas/pages/messages.ts @@ -9,7 +9,8 @@ export class SchemaUpdated { constructor( public readonly name: string, public readonly label: string, - public readonly isPublished: boolean + public readonly isPublished: boolean, + public readonly version: string ) { } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts index 4cee9eb3d..a80ca2bc0 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts @@ -11,7 +11,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Notification, NotificationService, - SchemasService + SchemasService, + Version } from 'shared'; import { SchemaPropertiesDto } from './schema-properties'; @@ -31,6 +32,9 @@ export class SchemaEditFormComponent implements OnInit { @Input() public schema: SchemaPropertiesDto; + @Input() + public version: Version; + @Input() public appName: string; @@ -72,7 +76,7 @@ export class SchemaEditFormComponent implements OnInit { const requestDto = this.editForm.value; - this.schemas.putSchema(this.appName, this.schema.name, requestDto) + this.schemas.putSchema(this.appName, this.schema.name, requestDto, this.version) .subscribe(dto => { this.reset(); this.saved.emit(new SchemaPropertiesDto(this.schema.name, requestDto.label, requestDto.hints)); diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 0ded9fb1a..0bc795898 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -82,6 +82,7 @@ diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index d3289f38b..afd1ebf73 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -25,7 +25,8 @@ import { SchemasService, UpdateFieldDto, UsersProviderService, - ValidatorsEx + ValidatorsEx, + Version } from 'shared'; import { SchemaPropertiesDto } from './schema-properties'; @@ -52,6 +53,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public schemaFields = ImmutableArray.empty(); public schemaProperties: SchemaPropertiesDto; + public version = new Version(''); + public editSchemaDialog = new ModalView(); public isPublished: boolean; @@ -86,13 +89,15 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.schemaFields = ImmutableArray.of(schema.fields); this.schemaProperties = new SchemaPropertiesDto(schema.name, schema.label, schema.hints); + this.version = schema.version; + this.isPublished = schema.isPublished; }); } public publish() { this.appName() - .switchMap(app => this.schemasService.publishSchema(app, this.schemaName)).retry(2) + .switchMap(app => this.schemasService.publishSchema(app, this.schemaName, this.version)).retry(2) .subscribe(() => { this.isPublished = true; this.notify(); @@ -103,7 +108,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public unpublish() { this.appName() - .switchMap(app => this.schemasService.unpublishSchema(app, this.schemaName)).retry(2) + .switchMap(app => this.schemasService.unpublishSchema(app, this.schemaName, this.version)).retry(2) .subscribe(() => { this.isPublished = false; this.notify(); @@ -114,7 +119,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public enableField(field: FieldDto) { this.appName() - .switchMap(app => this.schemasService.enableField(app, this.schemaName, field.fieldId)).retry(2) + .switchMap(app => this.schemasService.enableField(app, this.schemaName, field.fieldId, this.version)).retry(2) .subscribe(() => { this.updateField(field, new FieldDto(field.fieldId, field.name, field.isHidden, false, field.properties)); }, error => { @@ -124,7 +129,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public disableField(field: FieldDto) { this.appName() - .switchMap(app => this.schemasService.disableField(app, this.schemaName, field.fieldId)).retry(2) + .switchMap(app => this.schemasService.disableField(app, this.schemaName, field.fieldId, this.version)).retry(2) .subscribe(() => { this.updateField(field, new FieldDto(field.fieldId, field.name, field.isHidden, true, field.properties)); }, error => { @@ -134,7 +139,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public showField(field: FieldDto) { this.appName() - .switchMap(app => this.schemasService.showField(app, this.schemaName, field.fieldId)).retry(2) + .switchMap(app => this.schemasService.showField(app, this.schemaName, field.fieldId, this.version)).retry(2) .subscribe(() => { this.updateField(field, new FieldDto(field.fieldId, field.name, false, field.isDisabled, field.properties)); }, error => { @@ -144,7 +149,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public hideField(field: FieldDto) { this.appName() - .switchMap(app => this.schemasService.hideField(app, this.schemaName, field.fieldId)).retry(2) + .switchMap(app => this.schemasService.hideField(app, this.schemaName, field.fieldId, this.version)).retry(2) .subscribe(() => { this.updateField(field, new FieldDto(field.fieldId, field.name, true, field.isDisabled, field.properties)); }, error => { @@ -154,7 +159,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { public deleteField(field: FieldDto) { this.appName() - .switchMap(app => this.schemasService.deleteField(app, this.schemaName, field.fieldId)).retry(2) + .switchMap(app => this.schemasService.deleteField(app, this.schemaName, field.fieldId, this.version)).retry(2) .subscribe(() => { this.updateFields(this.schemaFields.remove(field)); }, error => { @@ -166,7 +171,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { const request = new UpdateFieldDto(newField.properties); this.appName() - .switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, request)).retry(2) + .switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, request, this.version)).retry(2) .subscribe(() => { this.updateField(field, new FieldDto(field.fieldId, field.name, newField.isHidden, field.isDisabled, newField.properties)); }, error => { @@ -191,7 +196,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { }; this.appName() - .switchMap(app => this.schemasService.postField(app, this.schemaName, requestDto)) + .switchMap(app => this.schemasService.postField(app, this.schemaName, requestDto, this.version)) .subscribe(dto => { const newField = new FieldDto(parseInt(dto.id, 10), @@ -240,7 +245,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { private notify() { this.messageBus.publish(new HistoryChannelUpdated()); - this.messageBus.publish(new SchemaUpdated(this.schemaName, this.schemaProperties.label, this.isPublished)); + this.messageBus.publish(new SchemaUpdated(this.schemaName, this.schemaProperties.label, this.isPublished, this.version.value)); } } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts index cf3ffcfec..43e662008 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts @@ -17,7 +17,8 @@ import { fadeAnimation, SchemaDto, SchemasService, - ValidatorsEx + ValidatorsEx, + Version } from 'shared'; const FALLBACK_NAME = 'my-schema'; @@ -75,14 +76,15 @@ export class SchemaFormComponent { if (this.createForm.valid) { this.createForm.disable(); - const name = this.createForm.get('name').value; + const schemaVersion = new Version(); + const schemaName = this.createForm.get('name').value; - const requestDto = new CreateSchemaDto(name); + const requestDto = new CreateSchemaDto(schemaName); - this.schemas.postSchema(this.appName, requestDto) + this.schemas.postSchema(this.appName, requestDto, schemaVersion) .subscribe(dto => { this.reset(); - this.created.emit(this.createSchemaDto(dto.id, name)); + this.created.emit(this.createSchemaDto(dto.id, schemaName, schemaVersion)); }, error => { this.createForm.enable(); this.creationError = error.displayMessage; @@ -96,10 +98,10 @@ export class SchemaFormComponent { this.createFormSubmitted = false; } - private createSchemaDto(id: string, name: string) { + private createSchemaDto(id: string, name: string, version: Version) { const user = this.authService.user!.token; const now = DateTime.now(); - return new SchemaDto(id, name, undefined, false, user, user, now, now); + return new SchemaDto(id, name, undefined, false, user, user, now, now, version); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 1817ffaa5..a25f8d65e 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -22,7 +22,8 @@ import { NotificationService, SchemaDto, SchemasService, - UsersProviderService + UsersProviderService, + Version } from 'shared'; import { SchemaUpdated } from './../messages'; @@ -130,6 +131,7 @@ function updateSchema(schema: SchemaDto, authService: AuthService, message: Sche message.label, message.isPublished, schema.createdBy, me, - schema.created, DateTime.now()); + schema.created, DateTime.now(), + new Version(message.version)); } diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index a6cb2a298..0fc70eca7 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -22,7 +22,7 @@ - diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts index 49bbf37d3..4b60c501e 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts @@ -20,7 +20,8 @@ import { NotificationService, UpdateAppClientDto, UsersProviderService, - ValidatorsEx + ValidatorsEx, + Version } from 'shared'; @Component({ @@ -29,6 +30,8 @@ import { templateUrl: './clients-page.component.html' }) export class ClientsPageComponent extends AppComponentBase implements OnInit { + private version = new Version(); + public appClients: ImmutableArray; public addClientForm: FormGroup = @@ -55,7 +58,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { public load() { this.appName() - .switchMap(app => this.appClientsService.getClients(app).retry(2)) + .switchMap(app => this.appClientsService.getClients(app, this.version).retry(2)) .subscribe(dtos => { this.updateClients(ImmutableArray.of(dtos)); }, error => { @@ -65,7 +68,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { public revokeClient(client: AppClientDto) { this.appName() - .switchMap(app => this.appClientsService.deleteClient(app, client.id)) + .switchMap(app => this.appClientsService.deleteClient(app, client.id, this.version)) .subscribe(() => { this.updateClients(this.appClients.remove(client)); }, error => { @@ -77,7 +80,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { const request = new UpdateAppClientDto(name); this.appName() - .switchMap(app => this.appClientsService.updateClient(app, client.id, request)) + .switchMap(app => this.appClientsService.updateClient(app, client.id, request, this.version)) .subscribe(() => { this.updateClients(this.appClients.replace(client, rename(client, name))); }, error => { @@ -103,7 +106,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { }; this.appName() - .switchMap(app => this.appClientsService.postClient(app, requestDto)) + .switchMap(app => this.appClientsService.postClient(app, requestDto, this.version)) .subscribe(dto => { this.updateClients(this.appClients.push(dto)); reset(); diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index 0338e65ac..06abbdfd8 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -22,7 +22,8 @@ import { MessageBus, NotificationService, UsersProviderService, - UsersService + UsersService, + Version } from 'shared'; export class UsersDataSource implements AutocompleteSource { @@ -58,6 +59,8 @@ export class UsersDataSource implements AutocompleteSource { templateUrl: './contributors-page.component.html' }) export class ContributorsPageComponent extends AppComponentBase implements OnInit { + private version = new Version(); + public appContributors = ImmutableArray.empty(); public currentUserId: string; @@ -96,7 +99,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni public load() { this.appName() - .switchMap(app => this.appContributorsService.getContributors(app).retry(2)) + .switchMap(app => this.appContributorsService.getContributors(app, this.version).retry(2)) .subscribe(dtos => { this.updateContributors(ImmutableArray.of(dtos)); }, error => { @@ -106,7 +109,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni public removeContributor(contributor: AppContributorDto) { this.appName() - .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId)) + .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId, this.version)) .subscribe(() => { this.updateContributors(this.appContributors.remove(contributor)); }, error => { @@ -118,7 +121,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni const newContributor = new AppContributorDto(this.addContributorForm.get('user').value.model.id, 'Editor'); this.appName() - .switchMap(app => this.appContributorsService.postContributor(app, newContributor)) + .switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version)) .subscribe(() => { this.updateContributors(this.appContributors.push(newContributor)); }, error => { @@ -132,7 +135,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni const newContributor = changePermission(contributor, permission); this.appName() - .switchMap(app => this.appContributorsService.postContributor(app, newContributor)) + .switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version)) .subscribe(() => { this.updateContributors(this.appContributors.replace(contributor, newContributor)); }, error => { diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts index 9bba7aa20..f1d8c6940 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts @@ -21,7 +21,8 @@ import { LanguageService, NotificationService, UpdateAppLanguageDto, - UsersProviderService + UsersProviderService, + Version } from 'shared'; @Component({ @@ -30,6 +31,8 @@ import { templateUrl: './languages-page.component.html' }) export class LanguagesPageComponent extends AppComponentBase implements OnInit { + private version = new Version(); + public allLanguages: LanguageDto[] = []; public appLanguages = ImmutableArray.empty(); @@ -66,7 +69,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { public load() { this.appName() - .switchMap(app => this.appLanguagesService.getLanguages(app).retry(2)) + .switchMap(app => this.appLanguagesService.getLanguages(app, this.version).retry(2)) .subscribe(dtos => { this.updateLanguages(ImmutableArray.of(dtos)); }, error => { @@ -76,9 +79,9 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { public removeLanguage(language: AppLanguageDto) { this.appName() - .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code)) + .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code, this.version)) .subscribe(dto => { - this.updateLanguages(this.appLanguages.remove(dto)); + this.updateLanguages(this.appLanguages.remove(language)); }, error => { this.notifyError(error); }); @@ -88,7 +91,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { const request = new AddAppLanguageDto(this.addLanguageForm.get('language').value.iso2Code); this.appName() - .switchMap(app => this.appLanguagesService.postLanguages(app, request)) + .switchMap(app => this.appLanguagesService.postLanguages(app, request, this.version)) .subscribe(dto => { this.updateLanguages(this.appLanguages.push(dto)); }, error => { @@ -100,7 +103,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { const request = new UpdateAppLanguageDto(true); this.appName() - .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, request)) + .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, request, this.version)) .subscribe(() => { this.updateLanguages(this.appLanguages.map(l => { const isMasterLanguage = l === language; @@ -114,6 +117,8 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { }, error => { this.notifyError(error); }); + + return false; } private updateLanguages(languages: ImmutableArray) { diff --git a/src/Squidex/app/framework/angular/http-utils.ts b/src/Squidex/app/framework/angular/http-utils.ts index 301a35c12..3aad1e690 100644 --- a/src/Squidex/app/framework/angular/http-utils.ts +++ b/src/Squidex/app/framework/angular/http-utils.ts @@ -53,10 +53,15 @@ export function catchError(message: string): Observable { return this.catch((error: any | Response) => { let result = new ErrorDto(500, message); - if (error instanceof Response && error.status !== 500) { + if (error instanceof Response) { const body = error.json(); - result = new ErrorDto(error.status, body.message, body.details); + 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); + } + } return Observable.throw(result); diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 2f32c222e..262053dbb 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -49,4 +49,5 @@ export * from './utils/duration'; export * from './utils/immutable-array'; export * from './utils/math-helper'; export * from './utils/modal-view'; -export * from './utils/string-helper'; \ No newline at end of file +export * from './utils/string-helper'; +export * from './utils/version'; \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/version.ts b/src/Squidex/app/framework/utils/version.ts new file mode 100644 index 000000000..e06958b00 --- /dev/null +++ b/src/Squidex/app/framework/utils/version.ts @@ -0,0 +1,21 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export class Version { + public get value() { + return this.currentValue; + } + + constructor( + private currentValue: string = '' + ) { + } + + public update(newValue: string) { + this.currentValue = newValue; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts index 40c19615f..2ca36223a 100644 --- a/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts @@ -33,7 +33,7 @@ describe('ResolveAppLanguagesGuard', () => { }); it('should navigate to 404 page if languages are not found', (done) => { - appLanguagesService.setup(x => x.getLanguages('my-app')) + appLanguagesService.setup(x => x.getLanguages('my-app', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -49,7 +49,7 @@ describe('ResolveAppLanguagesGuard', () => { }); it('should navigate to 404 page if languages loading fails', (done) => { - appLanguagesService.setup(x => x.getLanguages('my-app')) + appLanguagesService.setup(x => x.getLanguages('my-app', null)) .returns(() => Observable.throw(null!)); const router = new RouterMockup(); @@ -67,7 +67,7 @@ describe('ResolveAppLanguagesGuard', () => { it('should return schema if loading succeeded', (done) => { const languages: AppLanguageDto[] = []; - appLanguagesService.setup(x => x.getLanguages('my-app')) + appLanguagesService.setup(x => x.getLanguages('my-app', null)) .returns(() => Observable.of(languages)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts b/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts index 920a0094f..ced6111c2 100644 --- a/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts @@ -26,7 +26,7 @@ export class ResolveAppLanguagesGuard implements Resolve { } const result = - this.appLanguagesService.getLanguages(appName).toPromise() + this.appLanguagesService.getLanguages(appName, null).toPromise() .then(dto => { if (!dto) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts index f0ea8c9b8..52ef46ec9 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts @@ -43,7 +43,7 @@ describe('ResolveContentGuard', () => { }); it('should navigate to 404 page if schema is not found', (done) => { - appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) + appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -59,7 +59,7 @@ describe('ResolveContentGuard', () => { }); it('should navigate to 404 page if schema loading fails', (done) => { - appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) + appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null)) .returns(() => Observable.throw(null!)); const router = new RouterMockup(); @@ -77,7 +77,7 @@ describe('ResolveContentGuard', () => { it('should return schema if loading succeeded', (done) => { const schema = {}; - appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) + appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.ts b/src/Squidex/app/shared/guards/resolve-content.guard.ts index 461028e88..c4cbbcb90 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-content.guard.ts @@ -28,7 +28,7 @@ export class ResolveContentGuard implements Resolve { } const result = - this.contentsService.getContent(appName, schemaName, contentId).toPromise() + this.contentsService.getContent(appName, schemaName, contentId, null).toPromise() .then(dto => { if (!dto) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts index 3b9dfee16..e76b358a2 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts @@ -38,7 +38,7 @@ describe('ResolvePublishedSchemaGuard', () => { }); it('should navigate to 404 page if schema is not found', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -54,7 +54,7 @@ describe('ResolvePublishedSchemaGuard', () => { }); it('should navigate to 404 page if schema loading fails', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.throw(null)); const router = new RouterMockup(); @@ -72,7 +72,7 @@ describe('ResolvePublishedSchemaGuard', () => { it('should navigate to 404 page if schema not published', (done) => { const schema = { isPublished: false }; - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); @@ -90,7 +90,7 @@ describe('ResolvePublishedSchemaGuard', () => { it('should return schema if loading succeeded', (done) => { const schema = { isPublished: true }; - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts index 0cbdecc66..956fc1c4e 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts @@ -27,7 +27,7 @@ export class ResolvePublishedSchemaGuard implements Resolve { } const result = - this.schemasService.getSchema(appName, schemaName).toPromise() + this.schemasService.getSchema(appName, schemaName, null).toPromise() .then(dto => { if (!dto || !dto.isPublished) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts index aaacd1ad7..7f7f9ffc6 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts @@ -38,7 +38,7 @@ describe('ResolveSchemaGuard', () => { }); it('should navigate to 404 page if schema is not found', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -54,7 +54,7 @@ describe('ResolveSchemaGuard', () => { }); it('should navigate to 404 page if schema loading fails', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.throw(null!)); const router = new RouterMockup(); @@ -72,7 +72,7 @@ describe('ResolveSchemaGuard', () => { it('should return schema if loading succeeded', (done) => { const schema = {}; - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.ts index ac9cc8752..293f1ca92 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.ts @@ -27,7 +27,7 @@ export class ResolveSchemaGuard implements Resolve { } const result = - this.schemasService.getSchema(appName, schemaName).toPromise() + this.schemasService.getSchema(appName, schemaName, null).toPromise() .then(dto => { if (!dto) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/services/app-clients.service.spec.ts b/src/Squidex/app/shared/services/app-clients.service.spec.ts index ed93d2ac3..d68bffd90 100644 --- a/src/Squidex/app/shared/services/app-clients.service.spec.ts +++ b/src/Squidex/app/shared/services/app-clients.service.spec.ts @@ -16,12 +16,14 @@ import { AppClientsService, AuthService, CreateAppClientDto, - UpdateAppClientDto + UpdateAppClientDto, + Version } from './../'; describe('AppClientsService', () => { let authService: IMock; let appClientsService: AppClientsService; + let version = new Version('1'); let http: IMock; beforeEach(() => { @@ -32,7 +34,7 @@ describe('AppClientsService', () => { }); it('should make get request to get app clients', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/clients')) + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/clients', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -52,7 +54,7 @@ describe('AppClientsService', () => { let clients: AppClientDto[] | null = null; - appClientsService.getClients('my-app').subscribe(result => { + appClientsService.getClients('my-app', version).subscribe(result => { clients = result; }).unsubscribe(); @@ -68,7 +70,7 @@ describe('AppClientsService', () => { it('should make post request to create client', () => { const dto = new CreateAppClientDto('client1'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -84,7 +86,7 @@ describe('AppClientsService', () => { let client: AppClientDto | null = null; - appClientsService.postClient('my-app', dto).subscribe(result => { + appClientsService.postClient('my-app', dto, version).subscribe(result => { client = result; }); @@ -97,7 +99,7 @@ describe('AppClientsService', () => { it('should make put request to rename client', () => { const dto = new UpdateAppClientDto('Client 1 New'); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -105,13 +107,13 @@ describe('AppClientsService', () => { )) .verifiable(Times.once()); - appClientsService.updateClient('my-app', 'client1', dto); + appClientsService.updateClient('my-app', 'client1', dto, version); authService.verifyAll(); }); it('should make delete request to remove client', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/clients/client1')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/clients/client1', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -119,7 +121,7 @@ describe('AppClientsService', () => { )) .verifiable(Times.once()); - appClientsService.deleteClient('my-app', 'client1'); + appClientsService.deleteClient('my-app', 'client1', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/app-clients.service.ts b/src/Squidex/app/shared/services/app-clients.service.ts index f15a35c00..cac70d755 100644 --- a/src/Squidex/app/shared/services/app-clients.service.ts +++ b/src/Squidex/app/shared/services/app-clients.service.ts @@ -11,7 +11,7 @@ import { Observable } from 'rxjs'; import 'framework/angular/http-extensions'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; import { AuthService } from './auth.service'; export class AppClientDto { @@ -54,10 +54,10 @@ export class AppClientsService { ) { } - public getClients(appName: string): Observable { + public getClients(appName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { const items: any[] = response; @@ -72,10 +72,10 @@ export class AppClientsService { .catchError('Failed to load clients. Please reload.'); } - public postClient(appName: string, dto: CreateAppClientDto): Observable { + public postClient(appName: string, dto: CreateAppClientDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new AppClientDto( @@ -86,17 +86,17 @@ export class AppClientsService { .catchError('Failed to add client. Please reload.'); } - public updateClient(appName: string, id: string, dto: UpdateAppClientDto): Observable { + public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to revoke client. Please reload.'); } - public deleteClient(appName: string, id: string): Observable { + public deleteClient(appName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to revoke client. Please reload.'); } diff --git a/src/Squidex/app/shared/services/app-contributors.service.spec.ts b/src/Squidex/app/shared/services/app-contributors.service.spec.ts index e68d158ae..047da8cfa 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.spec.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.spec.ts @@ -13,12 +13,14 @@ import { ApiUrlConfig, AppContributorDto, AppContributorsService, - AuthService + AuthService, + Version } from './../'; describe('AppContributorsService', () => { let authService: IMock; let appContributorsService: AppContributorsService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -26,7 +28,7 @@ describe('AppContributorsService', () => { }); it('should make get request to get app contributors', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/contributors')) + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/contributors', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -44,7 +46,7 @@ describe('AppContributorsService', () => { let contributors: AppContributorDto[] | null = null; - appContributorsService.getContributors('my-app').subscribe(result => { + appContributorsService.getContributors('my-app', version).subscribe(result => { contributors = result; }).unsubscribe(); @@ -60,7 +62,7 @@ describe('AppContributorsService', () => { it('should make post request to assign contributor', () => { const dto = new AppContributorDto('123', 'Owner'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -68,13 +70,13 @@ describe('AppContributorsService', () => { )) .verifiable(Times.once()); - appContributorsService.postContributor('my-app', dto); + appContributorsService.postContributor('my-app', dto, version); authService.verifyAll(); }); it('should make delete request to remove contributor', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/contributors/123')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/contributors/123', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -82,7 +84,7 @@ describe('AppContributorsService', () => { )) .verifiable(Times.once()); - appContributorsService.deleteContributor('my-app', '123'); + appContributorsService.deleteContributor('my-app', '123', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/app-contributors.service.ts b/src/Squidex/app/shared/services/app-contributors.service.ts index a5cc54be5..229db6446 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.ts @@ -10,7 +10,7 @@ import { Observable } from 'rxjs'; import 'framework/angular/http-extensions'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; import { AuthService } from './auth.service'; export class AppContributorDto { @@ -29,10 +29,10 @@ export class AppContributorsService { ) { } - public getContributors(appName: string): Observable { + public getContributors(appName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { const items: any[] = response; @@ -46,17 +46,17 @@ export class AppContributorsService { .catchError('Failed to load contributors. Please reload.'); } - public postContributor(appName: string, dto: AppContributorDto): Observable { + public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .catchError('Failed to add contributors. Please reload.'); } - public deleteContributor(appName: string, contributorId: string): Observable { + public deleteContributor(appName: string, contributorId: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to delete contributors. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/app-languages.service.spec.ts b/src/Squidex/app/shared/services/app-languages.service.spec.ts index 75e4b0ce0..9014f7beb 100644 --- a/src/Squidex/app/shared/services/app-languages.service.spec.ts +++ b/src/Squidex/app/shared/services/app-languages.service.spec.ts @@ -15,12 +15,14 @@ import { AppLanguageDto, AppLanguagesService, AuthService, - UpdateAppLanguageDto + UpdateAppLanguageDto, + Version } from './../'; describe('AppLanguagesService', () => { let authService: IMock; let appLanguagesService: AppLanguagesService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -28,7 +30,7 @@ describe('AppLanguagesService', () => { }); it('should make get request to get app languages', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/languages')) + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/languages', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -47,7 +49,7 @@ describe('AppLanguagesService', () => { let languages: AppLanguageDto[] | null = null; - appLanguagesService.getLanguages('my-app').subscribe(result => { + appLanguagesService.getLanguages('my-app', version).subscribe(result => { languages = result; }).unsubscribe(); @@ -63,7 +65,7 @@ describe('AppLanguagesService', () => { it('should make post request to add language', () => { const dto = new AddAppLanguageDto('de'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -78,7 +80,7 @@ describe('AppLanguagesService', () => { let language: AppLanguageDto | null = null; - appLanguagesService.postLanguages('my-app', dto).subscribe(result => { + appLanguagesService.postLanguages('my-app', dto, version).subscribe(result => { language = result; }); @@ -91,7 +93,7 @@ describe('AppLanguagesService', () => { it('should make put request to make master language', () => { const dto = new UpdateAppLanguageDto(true); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -99,13 +101,13 @@ describe('AppLanguagesService', () => { )) .verifiable(Times.once()); - appLanguagesService.updateLanguage('my-app', 'de', dto); + appLanguagesService.updateLanguage('my-app', 'de', dto, version); authService.verifyAll(); }); it('should make delete request to remove language', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/languages/de')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/languages/de', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -113,7 +115,7 @@ describe('AppLanguagesService', () => { )) .verifiable(Times.once()); - appLanguagesService.deleteLanguage('my-app', 'de'); + appLanguagesService.deleteLanguage('my-app', 'de', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/app-languages.service.ts b/src/Squidex/app/shared/services/app-languages.service.ts index f527fe794..f0324db1d 100644 --- a/src/Squidex/app/shared/services/app-languages.service.ts +++ b/src/Squidex/app/shared/services/app-languages.service.ts @@ -10,7 +10,7 @@ import { Observable } from 'rxjs'; import 'framework/angular/http-extensions'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; import { AuthService } from './auth.service'; export class AppLanguageDto { @@ -44,10 +44,10 @@ export class AppLanguagesService { ) { } - public getLanguages(appName: string): Observable { + public getLanguages(appName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { const items: any[] = response; @@ -62,10 +62,10 @@ export class AppLanguagesService { .catchError('Failed to load languages. Please reload.'); } - public postLanguages(appName: string, dto: AddAppLanguageDto): Observable { + public postLanguages(appName: string, dto: AddAppLanguageDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new AppLanguageDto( @@ -76,17 +76,17 @@ export class AppLanguagesService { .catchError('Failed to add language. Please reload.'); } - public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto): Observable { + public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to change language. Please reload.'); } - public deleteLanguage(appName: string, languageCode: string): Observable { + public deleteLanguage(appName: string, languageCode: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to add language. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index 66814bad8..b695550c5 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -16,7 +16,7 @@ import { UserManager } from 'oidc-client'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; export class Profile { public get id(): string { @@ -169,46 +169,56 @@ export class AuthService { return resultPromise; } - public authGet(url: string, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authGet(url: string, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.get(url, options)); + return this.checkResponse(this.http.get(url, options), version); } - public authPut(url: string, data: any, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authPut(url: string, data: any, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.put(url, data, options)); + return this.checkResponse(this.http.put(url, data, options), version); } - public authDelete(url: string, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authDelete(url: string, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.delete(url, options)); + return this.checkResponse(this.http.delete(url, options), version); } - public authPost(url: string, data: any, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authPost(url: string, data: any, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.post(url, data, options)); + return this.checkResponse(this.http.post(url, data, options), version); } - private checkResponse(response: Observable) { - return response.catch((error: Response) => { - if (error.status === 401 || error.status === 404) { - this.logoutRedirect(); + private checkResponse(responseStream: Observable, version?: Version) { + return responseStream + .do((response: Response) => { + if (version && response.status.toString().indexOf('2') === 0) { + const etag = response.headers.get('etag'); - return Observable.empty(); - } else if (error.status === 403) { - this.router.navigate(['/404']); + if (etag) { + version.update(etag); + } + } + }) + .catch((error: Response) => { + if (error.status === 401 || error.status === 404) { + this.logoutRedirect(); - return Observable.empty(); - } - return Observable.throw(error); - }); + return Observable.empty(); + } else if (error.status === 403) { + this.router.navigate(['/404']); + + return Observable.empty(); + } + return Observable.throw(error); + }); } - private setRequestOptions(options?: RequestOptions) { + private setRequestOptions(options?: RequestOptions, version?: Version) { if (!options) { options = new RequestOptions(); } @@ -219,12 +229,17 @@ export class AuthService { } options.headers.append('Accept-Language', '*'); - options.headers.append('Pragma', 'no-cache'); + + if (version && version.value.length > 0) { + options.headers.append('If-Match', version.value); + } if (this.currentUser && this.currentUser.user) { options.headers.append('Authorization', `${this.currentUser.user.token_type} ${this.currentUser.user.access_token}`); } + options.headers.append('Pragma', 'no-cache'); + return options; } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 5521c3948..45def41b2 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -16,12 +16,14 @@ import { ContentDto, ContentsDto, ContentsService, - DateTime + DateTime, + Version } from './../'; describe('ContentsService', () => { let authService: IMock; let contentsService: ContentsService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -42,6 +44,7 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, data: {} }, { id: 'id2', @@ -50,6 +53,7 @@ describe('ContentsService', () => { createdBy: 'Created2', lastModified: '2017-10-12T10:10', lastModifiedBy: 'LastModifiedBy2', + version: 22, data: {} }] } @@ -66,8 +70,16 @@ describe('ContentsService', () => { expect(contents).toEqual( new ContentsDto(10, [ - new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}), - new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), {}) + new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + {}, + new Version('11')), + new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', + DateTime.parseISO_UTC('2016-10-12T10:10'), + DateTime.parseISO_UTC('2017-10-12T10:10'), + {}, + new Version('22')) ])); authService.verifyAll(); @@ -118,7 +130,7 @@ describe('ContentsService', () => { }); it('should make get request to get content', () => { - authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1?hidden=true')) + authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1?hidden=true', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -129,6 +141,7 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, data: {} } }) @@ -138,12 +151,16 @@ describe('ContentsService', () => { let content: ContentDto | null = null; - contentsService.getContent('my-app', 'my-schema', 'content1').subscribe(result => { + contentsService.getContent('my-app', 'my-schema', 'content1', version).subscribe(result => { content = result; }).unsubscribe(); expect(content).toEqual( - new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {})); + 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(); }); @@ -151,7 +168,7 @@ describe('ContentsService', () => { it('should make post request to create content', () => { const dto = {}; - authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto)) + authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -165,7 +182,7 @@ describe('ContentsService', () => { let created: EntityCreatedDto | null = null; - contentsService.postContent('my-app', 'my-schema', dto).subscribe(result => { + contentsService.postContent('my-app', 'my-schema', dto, version).subscribe(result => { created = result; }); @@ -178,7 +195,7 @@ describe('ContentsService', () => { it('should make put request to update content', () => { const dto = {}; - authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1', dto)) + authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -186,13 +203,13 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.putContent('my-app', 'my-schema', 'content1', dto); + contentsService.putContent('my-app', 'my-schema', 'content1', dto, version); authService.verifyAll(); }); it('should make put request to publish content', () => { - authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/publish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/publish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -200,13 +217,13 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.publishContent('my-app', 'my-schema', 'content1'); + contentsService.publishContent('my-app', 'my-schema', 'content1', version); authService.verifyAll(); }); it('should make put request to unpublish content', () => { - authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/unpublish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/unpublish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -214,13 +231,13 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.unpublishContent('my-app', 'my-schema', 'content1'); + contentsService.unpublishContent('my-app', 'my-schema', 'content1', version); authService.verifyAll(); }); it('should make delete request to delete content', () => { - authService.setup(x => x.authDelete('http://service/p/api/content/my-app/my-schema/content1')) + authService.setup(x => x.authDelete('http://service/p/api/content/my-app/my-schema/content1', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -228,7 +245,7 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.deleteContent('my-app', 'my-schema', 'content1'); + contentsService.deleteContent('my-app', 'my-schema', 'content1', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 50b5c46e5..69614d5c5 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -13,7 +13,8 @@ import 'framework/angular/http-extensions'; import { ApiUrlConfig, DateTime, - EntityCreatedDto + EntityCreatedDto, + Version } from 'framework'; import { AuthService } from './auth.service'; @@ -34,7 +35,8 @@ export class ContentDto { public readonly lastModifiedBy: string, public readonly created: DateTime, public readonly lastModified: DateTime, - public readonly data: any + public readonly data: any, + public readonly version: Version ) { } } @@ -83,16 +85,17 @@ export class ContentsService { item.lastModifiedBy, DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.lastModified), - item.data); + item.data, + new Version(item.version.toString())); })); }) .catchError('Failed to load contents. Please reload.'); } - public getContent(appName: string, schemaName: string, id: string): Observable { + public getContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}?hidden=true`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { return new ContentDto( @@ -102,15 +105,16 @@ export class ContentsService { response.lastModifiedBy, DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.lastModified), - response.data); + response.data, + new Version(response.version.toString())); }) .catchError('Failed to load content. Please reload.'); } - public postContent(appName: string, schemaName: string, dto: any): 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) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new EntityCreatedDto(response.id); @@ -118,31 +122,31 @@ export class ContentsService { .catchError('Failed to create content. Please reload.'); } - public putContent(appName: string, schemaName: string, id: string, dto: any): Observable { + public putContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to update content. Please reload.'); } - public publishContent(appName: string, schemaName: string, id: string): Observable { + public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to publish content. Please reload.'); } - public unpublishContent(appName: string, schemaName: string, id: string): Observable { + public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to unpublish content. Please reload.'); } - public deleteContent(appName: string, schemaName: string, id: string): Observable { + public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to delete content. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index 5b2fb5505..454de7e1a 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -22,12 +22,14 @@ import { SchemaDto, SchemasService, UpdateFieldDto, - UpdateSchemaDto + UpdateSchemaDto, + Version } from './../'; describe('SchemasService', () => { let authService: IMock; let schemasService: SchemasService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -52,6 +54,7 @@ describe('SchemasService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, data: {} }, { id: 'id2', @@ -62,6 +65,7 @@ describe('SchemasService', () => { createdBy: 'Created2', lastModified: '2017-10-12T10:10', lastModifiedBy: 'LastModifiedBy2', + version: 22, data: {} }] }) @@ -76,8 +80,14 @@ describe('SchemasService', () => { }).unsubscribe(); expect(schemas).toEqual([ - new SchemaDto('id1', 'name1', 'label1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10')), - new SchemaDto('id2', 'name2', 'label2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10')) + new SchemaDto('id1', 'name1', 'label1', true, 'Created1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + new Version('11')), + new SchemaDto('id2', 'name2', 'label2', true, 'Created2', 'LastModifiedBy2', + DateTime.parseISO_UTC('2016-10-12T10:10'), + DateTime.parseISO_UTC('2017-10-12T10:10'), + new Version('22')) ]); authService.verifyAll(); @@ -98,13 +108,14 @@ describe('SchemasService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, fields: [{ fieldId: 1, name: 'field1', isHidden: true, isDisabled: true, properties: { - fieldType: 'number' + fieldType: 'Number' } }, { fieldId: 2, @@ -112,7 +123,7 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'string' + fieldType: 'String' } }, { fieldId: 3, @@ -120,7 +131,7 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'boolean' + fieldType: 'Boolean' } }, { fieldId: 4, @@ -128,7 +139,7 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'dateTime' + fieldType: 'DateTime' } }, { fieldId: 5, @@ -136,7 +147,7 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'json' + fieldType: 'Json' } }] } @@ -147,19 +158,21 @@ describe('SchemasService', () => { let schema: SchemaDetailsDto | null = null; - schemasService.getSchema('my-app', 'my-schema').subscribe(result => { + schemasService.getSchema('my-app', 'my-schema', version).subscribe(result => { schema = result; }).unsubscribe(); expect(schema).toEqual( new SchemaDetailsDto('id1', 'name1', 'label1', 'hints1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T10:10'), [ - new FieldDto(1, 'field1', true, true, createProperties('number')), - new FieldDto(2, 'field2', true, true, createProperties('string')), - new FieldDto(3, 'field3', true, true, createProperties('boolean')), - new FieldDto(4, 'field4', true, true, createProperties('dateTime')), - new FieldDto(5, 'field5', true, true, createProperties('json')) + DateTime.parseISO_UTC('2017-12-12T10:10'), + new Version('11'), + [ + new FieldDto(1, 'field1', true, true, createProperties('Number')), + new FieldDto(2, 'field2', true, true, createProperties('String')), + new FieldDto(3, 'field3', true, true, createProperties('Boolean')), + new FieldDto(4, 'field4', true, true, createProperties('DateTime')), + new FieldDto(5, 'field5', true, true, createProperties('Json')) ])); authService.verifyAll(); @@ -168,7 +181,7 @@ describe('SchemasService', () => { it('should make post request to create schema', () => { const dto = new CreateSchemaDto('name'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -182,7 +195,7 @@ describe('SchemasService', () => { let created: EntityCreatedDto | null = null; - schemasService.postSchema('my-app', dto).subscribe(result => { + schemasService.postSchema('my-app', dto, version).subscribe(result => { created = result; }); @@ -193,9 +206,9 @@ describe('SchemasService', () => { }); it('should make post request to add field', () => { - const dto = new AddFieldDto('name', createProperties('number')); + const dto = new AddFieldDto('name', createProperties('Number')); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/fields', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/fields', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -209,7 +222,7 @@ describe('SchemasService', () => { let created: EntityCreatedDto | null = null; - schemasService.postField('my-app', 'my-schema', dto).subscribe(result => { + schemasService.postField('my-app', 'my-schema', dto, version).subscribe(result => { created = result; }); @@ -222,7 +235,7 @@ describe('SchemasService', () => { it('should make put request to update schema', () => { const dto = new UpdateSchemaDto('label', 'hints'); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -230,15 +243,15 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.putSchema('my-app', 'my-schema', dto); + schemasService.putSchema('my-app', 'my-schema', dto, version); authService.verifyAll(); }); it('should make put request to update field', () => { - const dto = new UpdateFieldDto(createProperties('number')); + const dto = new UpdateFieldDto(createProperties('Number')); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -246,13 +259,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.putField('my-app', 'my-schema', 1, dto); + schemasService.putField('my-app', 'my-schema', 1, dto, version); authService.verifyAll(); }); it('should make put request to publish schema', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/publish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/publish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -260,13 +273,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.publishSchema('my-app', 'my-schema'); + schemasService.publishSchema('my-app', 'my-schema', version); authService.verifyAll(); }); it('should make put request to unpublish schema', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/unpublish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/unpublish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -274,13 +287,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.unpublishSchema('my-app', 'my-schema'); + schemasService.unpublishSchema('my-app', 'my-schema', version); authService.verifyAll(); }); it('should make put request to enable field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/enable', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/enable', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -288,13 +301,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.enableField('my-app', 'my-schema', 1); + schemasService.enableField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make put request to disable field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/disable', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/disable', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -302,13 +315,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.disableField('my-app', 'my-schema', 1); + schemasService.disableField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make put request to show field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/show', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/show', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -316,13 +329,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.showField('my-app', 'my-schema', 1); + schemasService.showField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make put request to hide field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/hide', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/hide', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -330,13 +343,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.hideField('my-app', 'my-schema', 1); + schemasService.hideField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make delete request to delete field', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/fields/1')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -344,7 +357,7 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.deleteField('my-app', 'my-schema', 1); + schemasService.deleteField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 12858fb06..db78030c1 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -13,7 +13,8 @@ import 'framework/angular/http-extensions'; import { ApiUrlConfig, DateTime, - EntityCreatedDto + EntityCreatedDto, + Version } from 'framework'; import { AuthService } from './auth.service'; @@ -69,7 +70,8 @@ export class SchemaDto { public readonly createdBy: string, public readonly lastModifiedBy: string, public readonly created: DateTime, - public readonly lastModified: DateTime + public readonly lastModified: DateTime, + public readonly version: Version ) { } } @@ -85,6 +87,7 @@ export class SchemaDetailsDto { public readonly lastModifiedBy: string, public readonly created: DateTime, public readonly lastModified: DateTime, + public readonly version: Version, public readonly fields: FieldDto[] ) { } @@ -246,13 +249,14 @@ export class SchemasService { item.createdBy, item.lastModifiedBy, DateTime.parseISO_UTC(item.created), - DateTime.parseISO_UTC(item.lastModified)); + DateTime.parseISO_UTC(item.lastModified), + new Version(item.version.toString())); }); }) .catchError('Failed to load schemas. Please reload.'); } - public getSchema(appName: string, id: string): Observable { + public getSchema(appName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`); return this.authService.authGet(url) @@ -282,15 +286,16 @@ export class SchemasService { response.lastModifiedBy, DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.lastModified), + new Version(response.version.toString()), fields); }) .catchError('Failed to load schema. Please reload.'); } - public postSchema(appName: string, dto: CreateSchemaDto): Observable { + public postSchema(appName: string, dto: CreateSchemaDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new EntityCreatedDto(response.id); @@ -298,10 +303,10 @@ export class SchemasService { .catchError('Failed to create schema. Please reload.'); } - public postField(appName: string, schemaName: string, dto: AddFieldDto): Observable { + public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new EntityCreatedDto(response.id); @@ -309,66 +314,66 @@ export class SchemasService { .catchError('Failed to add field. Please reload.'); } - public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto): Observable { + public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to update schema. Please reload.'); } - public publishSchema(appName: string, schemaName: string): Observable { + public publishSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to publish schema. Please reload.'); } - public unpublishSchema(appName: string, schemaName: string): Observable { + public unpublishSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to unpublish schema. Please reload.'); } - public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto): Observable { + public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to update field. Please reload.'); } - public enableField(appName: string, schemaName: string, fieldId: number): Observable { + public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to enable field. Please reload.'); } - public disableField(appName: string, schemaName: string, fieldId: number): Observable { + public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to disable field. Please reload.'); } - public showField(appName: string, schemaName: string, fieldId: number): Observable { + public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to show field. Please reload.'); } - public hideField(appName: string, schemaName: string, fieldId: number): Observable { + public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to hide field. Please reload.'); } - public deleteField(appName: string, schemaName: string, fieldId: number): Observable { + public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to delete field. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index beb1ee90a..1212c0680 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -151,8 +151,8 @@ body { } &-cancel { - padding: .2rem; - font-size: 1.5rem; + padding: .4rem; + font-size: 1.1rem; font-weight: normal; } From ec8ba7540d0b531d7a94bc000bb849dd7c3f1222 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 4 Mar 2017 19:33:04 +0100 Subject: [PATCH 20/66] Bugfixes for optimistic concurrency. --- .../EventStore/MongoEventStore.cs | 14 +++++++++----- src/Squidex.Infrastructure/CQRS/CommonHeaders.cs | 2 ++ .../CQRS/EnvelopeExtensions.cs | 12 ++++++++++++ .../CQRS/Events/EventReceiver.cs | 1 + .../CQRS/Events/StoredEvent.cs | 10 ++++++++-- src/Squidex.Read.MongoDb/Utils/EntityMapper.cs | 2 +- .../pages/content/content-page.component.ts | 2 +- .../Commands/DefaultDomainObjectRepositoryTests.cs | 8 ++++---- .../CQRS/EnvelopeExtensionsTests.cs | 11 +++++++++++ .../CQRS/Events/EventDataFormatterTests.cs | 1 + .../CQRS/Events/EventReceiverTests.cs | 6 +++--- 11 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs index d51cfc9c7..b7d3ff079 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs @@ -69,15 +69,17 @@ namespace Squidex.Infrastructure.MongoDb.EventStore { await Collection.Find(x => x.EventStream == streamName).ForEachAsync(commit => { - var position = commit.EventStreamOffset; + var eventNumber = commit.EventsOffset; + var eventStreamNumber = commit.EventStreamOffset; foreach (var @event in commit.Events) { - var eventData = SimpleMapper.Map(@event, new EventData()); + eventNumber++; + eventStreamNumber++; - observer.OnNext(new StoredEvent(position, eventData)); + var eventData = SimpleMapper.Map(@event, new EventData()); - position++; + observer.OnNext(new StoredEvent(eventNumber, eventStreamNumber, eventData)); } }, ct); }); @@ -92,16 +94,18 @@ namespace Squidex.Infrastructure.MongoDb.EventStore await Collection.Find(x => x.EventsOffset >= commitOffset).SortBy(x => x.EventsOffset).ForEachAsync(commit => { var eventNumber = commit.EventsOffset; + var eventStreamNumber = commit.EventStreamOffset; foreach (var @event in commit.Events) { eventNumber++; + eventStreamNumber++; if (eventNumber > lastReceivedEventNumber) { var eventData = SimpleMapper.Map(@event, new EventData()); - observer.OnNext(new StoredEvent(eventNumber, eventData)); + observer.OnNext(new StoredEvent(eventNumber, eventStreamNumber, eventData)); } } }, ct); diff --git a/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs b/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs index 9274e62e2..c48283347 100644 --- a/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs +++ b/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs @@ -18,6 +18,8 @@ namespace Squidex.Infrastructure.CQRS public static readonly string EventNumber = "EventNumber"; + public static readonly string EventStreamNumber = "EventStreamNumber"; + public static readonly string Timestamp = "Timestamp"; public static readonly string Actor = "Actor"; diff --git a/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs b/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs index 27a757517..322369def 100644 --- a/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs +++ b/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs @@ -26,6 +26,18 @@ namespace Squidex.Infrastructure.CQRS return envelope; } + public static long EventStreamNumber(this EnvelopeHeaders headers) + { + return headers[CommonHeaders.EventStreamNumber].ToInt32(CultureInfo.InvariantCulture); + } + + public static Envelope SetEventStreamNumber(this Envelope envelope, long value) where T : class + { + envelope.Headers.Set(CommonHeaders.EventStreamNumber, value); + + return envelope; + } + public static Guid CommitId(this EnvelopeHeaders headers) { return headers[CommonHeaders.CommitId].ToGuid(CultureInfo.InvariantCulture); diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs index d693f040f..80c5ff3ba 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs @@ -176,6 +176,7 @@ namespace Squidex.Infrastructure.CQRS.Events var @event = formatter.Parse(storedEvent.Data); @event.SetEventNumber(storedEvent.EventNumber); + @event.SetEventStreamNumber(storedEvent.EventStreamNumber); return @event; } diff --git a/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs b/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs index 4f8f04d6e..547956c13 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs @@ -11,6 +11,7 @@ namespace Squidex.Infrastructure.CQRS.Events public sealed class StoredEvent { private readonly long eventNumber; + private readonly long eventStreamNumber; private readonly EventData data; public long EventNumber @@ -18,18 +19,23 @@ namespace Squidex.Infrastructure.CQRS.Events get { return eventNumber; } } + public long EventStreamNumber + { + get { return eventStreamNumber; } + } + public EventData Data { get { return data; } } - public StoredEvent(long eventNumber, EventData data) + public StoredEvent(long eventNumber, long eventStreamNumber, EventData data) { Guard.NotNull(data, nameof(data)); this.data = data; - this.eventNumber = eventNumber; + this.eventStreamNumber = eventStreamNumber; } } } diff --git a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs index bff43a0be..c9830576a 100644 --- a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs @@ -62,7 +62,7 @@ namespace Squidex.Read.MongoDb.Utils if (withVersion != null) { - withVersion.Version = headers.EventNumber(); + withVersion.Version = headers.EventStreamNumber(); } } 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 be9fb02cd..91b147f95 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 @@ -39,7 +39,7 @@ import { }) export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit { private messageSubscription: Subscription; - private version: Version; + private version: Version = new Version(''); public schema: SchemaDetailsDto; diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs index 850366649..ffe4340e4 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs @@ -90,8 +90,8 @@ namespace Squidex.Infrastructure.CQRS.Commands var events = new[] { - new StoredEvent(0, eventData1), - new StoredEvent(1, eventData2) + new StoredEvent(0, 0, eventData1), + new StoredEvent(1, 1, eventData2) }; eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(events.ToObservable()); @@ -115,8 +115,8 @@ namespace Squidex.Infrastructure.CQRS.Commands var events = new[] { - new StoredEvent(0, eventData1), - new StoredEvent(1, eventData2) + new StoredEvent(0, 0, eventData1), + new StoredEvent(1, 1, eventData2) }; eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(events.ToObservable()); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs index 1e20276fa..eb0bc8436 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs @@ -72,5 +72,16 @@ namespace Squidex.Infrastructure.CQRS Assert.Equal(eventNumber, sut.Headers.EventNumber()); Assert.Equal(eventNumber, sut.Headers["EventNumber"].ToInt32(culture)); } + + [Fact] + public void Should_set_and_get_event_stream_number() + { + const int eventStreamNumber = 123; + + sut.SetEventStreamNumber(eventStreamNumber); + + Assert.Equal(eventStreamNumber, sut.Headers.EventStreamNumber()); + Assert.Equal(eventStreamNumber, sut.Headers["EventStreamNumber"].ToInt32(culture)); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs index a96515f52..e76e34877 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs @@ -42,6 +42,7 @@ namespace Squidex.Infrastructure.CQRS.Events inputEvent.SetCommitId(commitId); inputEvent.SetEventId(Guid.NewGuid()); inputEvent.SetEventNumber(1); + inputEvent.SetEventStreamNumber(1); inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); var sut = new EventDataFormatter(typeNameRegistry, serializerSettings); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs index e05616428..08d8aa308 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs @@ -81,9 +81,9 @@ namespace Squidex.Infrastructure.CQRS.Events { events = new[] { - new StoredEvent(3, eventData1), - new StoredEvent(4, eventData2), - new StoredEvent(4, eventData3) + new StoredEvent(3, 3, eventData1), + new StoredEvent(4, 4, eventData2), + new StoredEvent(4, 4, eventData3) }; consumerName = eventConsumer.Object.GetType().Name; From 72c68d7c69d9b585b5fae55871b98f6c9ce6f6fb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 4 Mar 2017 19:46:49 +0100 Subject: [PATCH 21/66] Sagger version bumped to get a bug fix --- src/Squidex.Core/Squidex.Core.csproj | 2 +- .../Squidex.Infrastructure.Redis.csproj | 2 +- src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj | 2 +- src/Squidex/Config/Swagger/SwaggerServices.cs | 12 ++++++++---- .../Controllers/Api/Schemas/SchemasController.cs | 2 +- .../Controllers/ContentApi/ContentsController.cs | 2 ++ src/Squidex/Squidex.csproj | 12 ++++++------ tests/RunCoverage.ps1 | 2 +- tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj | 8 ++++---- .../Squidex.Infrastructure.Tests.csproj | 8 ++++---- tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj | 8 ++++---- tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj | 8 ++++---- 12 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/Squidex.Core/Squidex.Core.csproj b/src/Squidex.Core/Squidex.Core.csproj index 8080fb0ce..4e751cfad 100644 --- a/src/Squidex.Core/Squidex.Core.csproj +++ b/src/Squidex.Core/Squidex.Core.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj index 68d07fc7b..724d33104 100644 --- a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj +++ b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj @@ -11,6 +11,6 @@ - + diff --git a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj index ab4e5369b..6b175a123 100644 --- a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj +++ b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Squidex/Config/Swagger/SwaggerServices.cs b/src/Squidex/Config/Swagger/SwaggerServices.cs index 75ac816fd..8eb5836fc 100644 --- a/src/Squidex/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Config/Swagger/SwaggerServices.cs @@ -26,13 +26,13 @@ namespace Squidex.Config.Swagger { services.AddSingleton(typeof(SwaggerOwinSettings), s => { - var options = s.GetService>().Value; + var urlOptions = s.GetService>().Value; var settings = new SwaggerOwinSettings { Title = "Squidex API Specification", IsAspNetCore = false } - .ConfigurePaths() + .ConfigurePaths(urlOptions) .ConfigureSchemaSettings() - .ConfigureIdentity(options); + .ConfigureIdentity(urlOptions); return settings; }); @@ -50,13 +50,17 @@ namespace Squidex.Config.Swagger return settings; } - private static SwaggerOwinSettings ConfigurePaths(this SwaggerOwinSettings settings) + private static SwaggerOwinSettings ConfigurePaths(this SwaggerOwinSettings settings, MyUrlsOptions urlOptions) { settings.SwaggerRoute = $"{Constants.ApiPrefix}/swagger/v1/swagger.json"; settings.PostProcess = document => { document.BasePath = Constants.ApiPrefix; + document.Info.ExtensionData = new Dictionary + { + ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } + }; }; settings.MiddlewareBasePath = Constants.ApiPrefix; diff --git a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs index e6b9cd0f9..a1e933331 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs @@ -42,7 +42,7 @@ namespace Squidex.Controllers.Api.Schemas } /// - /// Get app schemas. + /// Get schemas. /// /// /// 200 => Schemas returned. diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 4f6cf483f..16094c4dc 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; 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; @@ -30,6 +31,7 @@ namespace Squidex.Controllers.ContentApi [Authorize(Roles = SquidexRoles.AppEditor)] [ApiExceptionFilter] [ServiceFilter(typeof(AppFilterAttribute))] + [SwaggerIgnore] public class ContentsController : ControllerBase { private readonly ISchemaProvider schemas; diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 16f3012af..3a56acdc1 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -32,9 +32,9 @@ - + - + @@ -56,12 +56,12 @@ - + - + - - + + diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index c95c806c1..3303a8a1a 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -50,6 +50,6 @@ New-Item -ItemType directory -Path $reportsFolder -output:"$workingFolder\$reportsFolder\Read.xml" ` -oldStyle -&"$userProfile\.nuget\packages\ReportGenerator\2.5.2\tools\ReportGenerator.exe" ` +&"$userProfile\.nuget\packages\ReportGenerator\2.5.5\tools\ReportGenerator.exe" ` -reports:"$workingFolder\$reportsFolder\*.xml" ` -targetdir:"$workingFolder\$reportsFolder\Output" \ No newline at end of file diff --git a/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj b/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj index c8adff1a5..99050d467 100644 --- a/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj +++ b/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj @@ -11,10 +11,10 @@ - - - - + + + + diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 5bc0fe25e..dd2c8f2ad 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj b/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj index cf8496346..2771f70ad 100644 --- a/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj +++ b/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj @@ -13,11 +13,11 @@ - + - - - + + + diff --git a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj index acad1fcf1..14516ca32 100644 --- a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj +++ b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj @@ -12,11 +12,11 @@ - + - - - + + + From 8e9e1ea5d0b7dee83c4bdeb374c92139f37ea48d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 5 Mar 2017 11:11:02 +0100 Subject: [PATCH 22/66] Observable fixes --- .../pages/content/content-page.component.ts | 32 +++++++++-------- .../pages/contents/contents-page.component.ts | 36 ++++++++++--------- .../pages/schemas/schemas-page.component.ts | 4 +-- .../pages/schema/schema-page.component.ts | 15 ++++---- .../types/boolean-validation.component.ts | 4 +-- .../types/date-time-validation.component.ts | 4 +-- .../pages/schema/types/number-ui.component.ts | 4 +-- .../types/number-validation.component.ts | 4 +-- .../pages/schema/types/string-ui.component.ts | 15 ++++---- .../types/string-validation.component.ts | 8 ++--- .../pages/schemas/schema-form.component.ts | 5 ++- .../pages/schemas/schemas-page.component.ts | 11 +++--- .../angular/json-editor.component.ts | 25 ++++++------- .../shared/components/app-form.component.ts | 5 ++- .../app/shared/services/apps-store.service.ts | 7 ++-- .../pages/internal/profile-menu.component.ts | 17 ++++----- .../shell/pages/login/login-page.component.ts | 17 ++++----- .../pages/logout/logout-page.component.ts | 17 ++++----- 18 files changed, 122 insertions(+), 108 deletions(-) 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 91b147f95..0b15b1e4a 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 @@ -67,23 +67,27 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, public ngOnInit() { this.messageSubscription = - this.messageBus.of(ContentDeleted).subscribe(message => { - if (message.id === this.contentId) { - this.router.navigate(['../'], { relativeTo: this.route }); - } - }); + this.messageBus.of(ContentDeleted) + .subscribe(message => { + if (message.id === this.contentId) { + this.router.navigate(['../'], { relativeTo: this.route }); + } + }); - this.route.parent.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => { - this.languages = languages; - }); + this.route.parent.data.map(p => p['appLanguages']) + .subscribe((languages: AppLanguageDto[]) => { + this.languages = languages; + }); - this.route.parent.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => { - this.setupForm(schema); - }); + this.route.parent.data.map(p => p['schema']) + .subscribe((schema: SchemaDetailsDto) => { + this.setupForm(schema); + }); - this.route.data.map(p => p['content']).subscribe((content: ContentDto) => { - this.populateForm(content); - }); + this.route.data.map(p => p['content']) + .subscribe((content: ContentDto) => { + this.populateForm(content); + }); } public saveContent() { 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 7f9a92386..d7f693a04 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 @@ -84,27 +84,31 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public ngOnInit() { this.messageCreatedSubscription = - this.messageBus.of(ContentCreated).subscribe(message => { - this.itemLast++; - this.contentTotal++; - this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data, message.version)); - }); + this.messageBus.of(ContentCreated) + .subscribe(message => { + this.itemLast++; + this.contentTotal++; + this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data, message.version)); + }); this.messageUpdatedSubscription = - this.messageBus.of(ContentUpdated).subscribe(message => { - this.updateContents(message.id, undefined, message.data, message.version); - }); + this.messageBus.of(ContentUpdated) + .subscribe(message => { + this.updateContents(message.id, undefined, message.data, message.version); + }); - this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => { - this.languages = languages; - }); + this.route.data.map(p => p['appLanguages']) + .subscribe((languages: AppLanguageDto[]) => { + this.languages = languages; + }); - this.route.data.map(p => p['schema']).subscribe(schema => { - this.schema = schema; + this.route.data.map(p => p['schema']) + .subscribe(schema => { + this.schema = schema; - this.reset(); - this.load(); - }); + this.reset(); + this.load(); + }); } public search() { diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts index bc8ea2c6f..bb696d0a3 100644 --- a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts @@ -26,8 +26,8 @@ import { export class SchemasPageComponent extends AppComponentBase { public schemasFilter = new FormControl(); public schemasFiltered = - Observable.of(null) - .merge(this.schemasFilter.valueChanges) + this.schemasFilter.valueChanges + .startWith(null) .distinctUntilChanged() .debounceTime(300) .combineLatest(this.loadSchemas(), diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index afd1ebf73..2ec4e91e3 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -84,15 +84,16 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { } public ngOnInit() { - this.route.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => { - this.schemaName = schema.name; - this.schemaFields = ImmutableArray.of(schema.fields); - this.schemaProperties = new SchemaPropertiesDto(schema.name, schema.label, schema.hints); + this.route.data.map(p => p['schema']) + .subscribe((schema: SchemaDetailsDto) => { + this.schemaName = schema.name; + this.schemaFields = ImmutableArray.of(schema.fields); + this.schemaProperties = new SchemaPropertiesDto(schema.name, schema.label, schema.hints); - this.version = schema.version; + this.version = schema.version; - this.isPublished = schema.isPublished; - }); + this.isPublished = schema.isPublished; + }); } public publish() { diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts index 14205bac8..58eff1ab4 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts @@ -30,8 +30,8 @@ export class BooleanValidationComponent implements OnInit { new FormControl(this.properties.defaultValue)); this.hideDefaultValue = - Observable.of(this.properties.isRequired) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts index 0509f0179..c3d65a206 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts @@ -42,8 +42,8 @@ export class DateTimeValidationComponent implements OnInit { ])); this.hideDefaultValue = - Observable.of(this.properties.isRequired) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts index c455d5892..7f8edc9c0 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts @@ -46,8 +46,8 @@ export class NumberUIComponent implements OnDestroy, OnInit { new FormControl(this.properties.allowedValues, [])); this.hideAllowedValues = - Observable.of(this.properties.editor) - .merge(this.editForm.get('editor').valueChanges) + this.editForm.get('editor').valueChanges + .startWith(this.properties.editor) .map(x => !x || x === 'Input' || x === 'Textarea'); this.editorSubscription = diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts index dc494977e..51fc8b779 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts @@ -36,8 +36,8 @@ export class NumberValidationComponent implements OnInit { new FormControl(this.properties.defaultValue)); this.hideDefaultValue = - Observable.of(this.properties.isRequired) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts index 2f75f145a..bb8a8f923 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts @@ -46,15 +46,16 @@ export class StringUIComponent implements OnDestroy, OnInit { new FormControl(this.properties.allowedValues)); this.hideAllowedValues = - Observable.of(this.properties.editor) - .merge(this.editForm.get('editor').valueChanges) + this.editForm.get('editor').valueChanges + .startWith(this.properties.editor) .map(x => !x || x === 'Input' || x === 'TextArea' || x === 'RichText' || x === 'Markdown'); this.editorSubscription = - this.hideAllowedValues.subscribe(isSelection => { - if (isSelection) { - this.editForm.get('allowedValues').setValue(undefined); - } - }); + this.hideAllowedValues + .subscribe(isSelection => { + if (isSelection) { + this.editForm.get('allowedValues').setValue(undefined); + } + }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts index 265506ccb..0387c7099 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts @@ -49,13 +49,13 @@ export class StringValidationComponent implements OnDestroy, OnInit { new FormControl(this.properties.defaultValue)); this.hideDefaultValue = - Observable.of(false) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); this.hidePatternMessage = - Observable.of(false) - .merge(this.editForm.get('pattern').valueChanges) + this.editForm.get('pattern').valueChanges + .startWith('') .map(x => !x || x.trim().length === 0); this.patternSubscription = diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts index 43e662008..baeb2dae0 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts @@ -7,7 +7,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; import { ApiUrlConfig, @@ -54,8 +53,8 @@ export class SchemaFormComponent { }); public schemaName = - Observable.of(FALLBACK_NAME) - .merge(this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME)); + this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME) + .startWith(FALLBACK_NAME); constructor( public readonly apiUrl: ApiUrlConfig, diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index a25f8d65e..27ffcc4c3 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -67,11 +67,12 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, this.updateSchemas(this.schemas, this.schemaQuery = q); }); - this.route.params.map(q => q['showDialog']).subscribe(showDialog => { - if (showDialog) { - this.addSchemaDialog.show(); - } - }); + this.route.params.map(q => q['showDialog']) + .subscribe(showDialog => { + if (showDialog) { + this.addSchemaDialog.show(); + } + }); this.messageSubscription = this.messageBus.of(SchemaUpdated) diff --git a/src/Squidex/app/framework/angular/json-editor.component.ts b/src/Squidex/app/framework/angular/json-editor.component.ts index 564b457e0..739293039 100644 --- a/src/Squidex/app/framework/angular/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/json-editor.component.ts @@ -67,21 +67,22 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit } public ngAfterViewInit() { - this.valueChanged.debounceTime(1000).subscribe(() => { - const isValid = this.aceEditor.getSession().getAnnotations().length === 0; + this.valueChanged.debounceTime(1000) + .subscribe(() => { + const isValid = this.aceEditor.getSession().getAnnotations().length === 0; - if (!isValid) { - this.changeCallback(null); - } else { - try { - const value = JSON.parse(this.aceEditor.getValue()); - - this.changeCallback(value); - } catch (e) { + if (!isValid) { this.changeCallback(null); + } else { + try { + const value = JSON.parse(this.aceEditor.getValue()); + + this.changeCallback(value); + } catch (e) { + this.changeCallback(null); + } } - } - }); + }); this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js').then(() => { this.aceEditor = ace.edit(this.editor.nativeElement); diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/src/Squidex/app/shared/components/app-form.component.ts index 3866aa5d3..4ca6c71d2 100644 --- a/src/Squidex/app/shared/components/app-form.component.ts +++ b/src/Squidex/app/shared/components/app-form.component.ts @@ -7,7 +7,6 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; import { ApiUrlConfig, ValidatorsEx } from 'framework'; @@ -41,8 +40,8 @@ export class AppFormComponent { }); public appName = - Observable.of(FALLBACK_NAME) - .merge(this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME)); + this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME) + .startWith(FALLBACK_NAME); constructor( public readonly apiUrl: ApiUrlConfig, diff --git a/src/Squidex/app/shared/services/apps-store.service.ts b/src/Squidex/app/shared/services/apps-store.service.ts index a45c49c42..7fca62d99 100644 --- a/src/Squidex/app/shared/services/apps-store.service.ts +++ b/src/Squidex/app/shared/services/apps-store.service.ts @@ -74,9 +74,10 @@ export class AppsStoreService { } private load() { - this.appsService.getApps().subscribe(apps => { - this.apps$.next(apps); - }); + this.appsService.getApps() + .subscribe(apps => { + this.apps$.next(apps); + }); } public selectApp(name: string | null): Promise { diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts b/src/Squidex/app/shell/pages/internal/profile-menu.component.ts index fa8cb9de2..28aba25c5 100644 --- a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts +++ b/src/Squidex/app/shell/pages/internal/profile-menu.component.ts @@ -43,16 +43,17 @@ export class ProfileMenuComponent implements OnInit, OnDestroy { public ngOnInit() { this.authenticationSubscription = - this.auth.isAuthenticated.take(1).subscribe(() => { - const user = this.auth.user; + this.auth.isAuthenticated.take(1) + .subscribe(() => { + const user = this.auth.user; - if (user) { - this.profilePictureUrl = user.pictureUrl; - this.profileDisplayName = user.displayName; + if (user) { + this.profilePictureUrl = user.pictureUrl; + this.profileDisplayName = user.displayName; - this.isAdmin = user.isAdmin; - } - }); + this.isAdmin = user.isAdmin; + } + }); } public logout() { diff --git a/src/Squidex/app/shell/pages/login/login-page.component.ts b/src/Squidex/app/shell/pages/login/login-page.component.ts index 2aa92c2db..d4baf4e29 100644 --- a/src/Squidex/app/shell/pages/login/login-page.component.ts +++ b/src/Squidex/app/shell/pages/login/login-page.component.ts @@ -22,13 +22,14 @@ export class LoginPageComponent implements OnInit { } public ngOnInit() { - this.auth.loginRedirectComplete().subscribe( - () => { - this.router.navigate(['/app'], { replaceUrl: true }); - }, - () => { - this.router.navigate(['/'], { replaceUrl: true }); - } - ); + this.auth.loginRedirectComplete() + .subscribe( + () => { + this.router.navigate(['/app'], { replaceUrl: true }); + }, + () => { + this.router.navigate(['/'], { replaceUrl: true }); + } + ); } } \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/logout/logout-page.component.ts b/src/Squidex/app/shell/pages/logout/logout-page.component.ts index d189f4e5c..e17c1d1e1 100644 --- a/src/Squidex/app/shell/pages/logout/logout-page.component.ts +++ b/src/Squidex/app/shell/pages/logout/logout-page.component.ts @@ -22,13 +22,14 @@ export class LogoutPageComponent implements OnInit { } public ngOnInit() { - this.auth.logoutRedirectComplete().subscribe( - () => { - this.router.navigate(['/'], { replaceUrl: true }); - }, - () => { - this.router.navigate(['/'], { replaceUrl: true }); - } - ); + this.auth.logoutRedirectComplete() + .subscribe( + () => { + this.router.navigate(['/'], { replaceUrl: true }); + }, + () => { + this.router.navigate(['/'], { replaceUrl: true }); + } + ); } } \ No newline at end of file From 096f4c80e216b35e65359cf833fd143a29a305bb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 5 Mar 2017 14:45:28 +0100 Subject: [PATCH 23/66] Bugfix: Data must be cleaned when it comes from api. --- src/Squidex.Core/Contents/ContentData.cs | 12 ++++++++++ .../ContentApi/ContentsController.cs | 6 ++--- .../EnrichWithExpectedVersionHandler.cs | 1 - .../Contents/ContentDataTests.cs | 23 ++++++++++++++++++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Squidex.Core/Contents/ContentData.cs b/src/Squidex.Core/Contents/ContentData.cs index 2766b6251..3dd708763 100644 --- a/src/Squidex.Core/Contents/ContentData.cs +++ b/src/Squidex.Core/Contents/ContentData.cs @@ -62,6 +62,18 @@ namespace Squidex.Core.Contents return result; } + public ContentData ToCleaned() + { + var result = new ContentData(); + + foreach (var fieldValue in this.Where(x => x.Value != null)) + { + result[fieldValue.Key] = fieldValue.Value; + } + + return result; + } + public ContentData ToIdModel(Schema schema) { Guard.NotNull(schema, nameof(schema)); diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 16094c4dc..0c1d23e07 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -118,7 +118,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/")] public async Task PostContent([FromBody] ContentData request) { - var command = new CreateContent { Data = request, ContentId = Guid.NewGuid() }; + var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned() }; var context = await CommandBus.PublishAsync(command); @@ -132,7 +132,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/{id}")] public async Task PutContent(Guid id, [FromBody] ContentData request) { - var command = new UpdateContent { ContentId = id, Data = request }; + var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() }; await CommandBus.PublishAsync(command); @@ -143,7 +143,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/{id}")] public async Task PatchContent(Guid id, [FromBody] ContentData request) { - var command = new PatchContent { ContentId = id, Data = request }; + var command = new PatchContent { ContentId = id, Data = request.ToCleaned() }; await CommandBus.PublishAsync(command); diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs index 764d932cb..937cbd0c4 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs @@ -7,7 +7,6 @@ // ========================================================================== using System.Globalization; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Squidex.Infrastructure.CQRS.Commands; diff --git a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs index 68f2824e1..f16f3b91d 100644 --- a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs +++ b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs @@ -156,7 +156,28 @@ namespace Squidex.Core.Contents Assert.Equal(expected, actual); } - + + [Fact] + public void Should_remove_null_values_when_cleaning() + { + var expected = + new ContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + var input = + new ContentData() + .AddField("field1", null) + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + var actual = input.ToCleaned(); + + Assert.Equal(expected, actual); + } + [Fact] public void Should_provide_invariant_from_first_language() { From dc4b8dad5b10cc65eea166b45d482998c55bd1f9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 5 Mar 2017 15:18:46 +0100 Subject: [PATCH 24/66] Fixes suggested by NDepend --- .../{Cloneable.cs => CloneableBase.cs} | 6 ++--- src/Squidex.Core/Schemas/Field.cs | 3 ++- src/Squidex.Core/Schemas/FieldProperties.cs | 2 +- ...rties.cs => NamedElementPropertiesBase.cs} | 4 +-- src/Squidex.Core/Schemas/Schema.cs | 2 +- src/Squidex.Core/Schemas/SchemaProperties.cs | 2 +- .../Schemas/{ => Validators}/IValidator.cs | 2 +- src/Squidex.Events/SquidexEvent.cs | 2 +- .../InstantSerializer.cs | 25 ++++++------------- .../RefTokenSerializer.cs | 25 ++++++------------- .../CQRS/Commands/AggregateHandler.cs | 1 + .../{DomainObject.cs => DomainObjectBase.cs} | 4 +-- .../CQRS/{ => Events}/CommonHeaders.cs | 2 +- .../CQRS/Events/DefaultMemoryEventNotifier.cs | 2 +- .../CQRS/{ => Events}/Envelope.cs | 2 +- .../CQRS/{ => Events}/EnvelopeExtensions.cs | 2 +- .../CQRS/{ => Events}/EnvelopeHeaders.cs | 3 ++- .../CQRS/Events/EventReceiver.cs | 2 +- ...sableObject.cs => DisposableObjectBase.cs} | 4 +-- src/Squidex.Infrastructure/Language.cs | 2 +- src/Squidex.Infrastructure/Languages.cs | 4 +++ .../Timers/CompletionTimer.cs | 2 +- .../Apps/MongoAppRepository_EventHandling.cs | 1 - .../MongoContentRepository_EventHandling.cs | 1 - .../History/MongoHistoryEventRepository.cs | 1 - .../MongoSchemaRepository_EventHandling.cs | 1 - .../Utils/EntityMapper.cs | 2 +- .../Utils/MongoCollectionExtensions.cs | 2 +- .../Apps/AppHistoryEventsCreator.cs | 1 - .../Implementations/CachingAppProvider.cs | 2 +- .../Contents/Builders/EdmModelBuilder.cs | 2 +- .../Contents/ContentHistoryEventsCreator.cs | 1 - .../History/HistoryEventsCreatorBase.cs | 1 - .../History/IHistoryEventsCreator.cs | 1 - .../Schemas/SchemaHistoryEventsCreator.cs | 1 - .../Implementations/CachingSchemaProvider.cs | 2 +- ...hingProvider.cs => CachingProviderBase.cs} | 6 ++--- src/Squidex.Write/Apps/AppDomainObject.cs | 2 +- .../Contents/ContentDomainObject.cs | 2 +- .../Schemas/SchemaDomainObject.cs | 2 +- .../Controllers/ContentApi/PingController.cs | 2 +- src/Squidex/Controllers/ControllerBase.cs | 5 ---- .../Pipeline/ApiExceptionFilterAttribute.cs | 2 +- src/Squidex/Pipeline/AppFilterAttribute.cs | 1 + src/Squidex/Pipeline/RandomErrorAttribute.cs | 2 +- .../CQRS/Commands/AggregateHandlerTests.cs | 2 +- .../DefaultDomainObjectFactoryTests.cs | 2 +- .../DefaultDomainObjectRepositoryTests.cs | 2 +- ...ObjectTest.cs => DomainObjectBaseTests.cs} | 6 ++--- .../CQRS/Events/DefaultNameResolverTests.cs | 4 +-- .../{ => Events}/EnvelopeExtensionsTests.cs | 2 +- .../CQRS/{ => Events}/EnvelopeHeaderTests.cs | 2 +- ...tTests.cs => DisposableObjectBaseTests.cs} | 6 ++--- .../LanguageTests.cs | 4 +-- .../TestHelpers/AssertHelper.cs | 1 - .../TestHelpers/HandlerTestBase.cs | 2 +- tools/GenerateLanguages/Program.cs | 4 +++ 57 files changed, 81 insertions(+), 102 deletions(-) rename src/Squidex.Core/Schemas/{Cloneable.cs => CloneableBase.cs} (89%) rename src/Squidex.Core/Schemas/{NamedElementProperties.cs => NamedElementPropertiesBase.cs} (92%) rename src/Squidex.Core/Schemas/{ => Validators}/IValidator.cs (92%) rename src/Squidex.Infrastructure/CQRS/{DomainObject.cs => DomainObjectBase.cs} (94%) rename src/Squidex.Infrastructure/CQRS/{ => Events}/CommonHeaders.cs (94%) rename src/Squidex.Infrastructure/CQRS/{ => Events}/Envelope.cs (97%) rename src/Squidex.Infrastructure/CQRS/{ => Events}/EnvelopeExtensions.cs (98%) rename src/Squidex.Infrastructure/CQRS/{ => Events}/EnvelopeHeaders.cs (95%) rename src/Squidex.Infrastructure/{DisposableObject.cs => DisposableObjectBase.cs} (94%) rename src/Squidex.Read/Utils/{CachingProvider.cs => CachingProviderBase.cs} (83%) rename tests/Squidex.Infrastructure.Tests/CQRS/{DomainObjectTest.cs => DomainObjectBaseTests.cs} (96%) rename tests/Squidex.Infrastructure.Tests/CQRS/{ => Events}/EnvelopeExtensionsTests.cs (98%) rename tests/Squidex.Infrastructure.Tests/CQRS/{ => Events}/EnvelopeHeaderTests.cs (97%) rename tests/Squidex.Infrastructure.Tests/{DisposableObjectTests.cs => DisposableObjectBaseTests.cs} (94%) diff --git a/src/Squidex.Core/Schemas/Cloneable.cs b/src/Squidex.Core/Schemas/CloneableBase.cs similarity index 89% rename from src/Squidex.Core/Schemas/Cloneable.cs rename to src/Squidex.Core/Schemas/CloneableBase.cs index 98908d4c6..6b1eecefe 100644 --- a/src/Squidex.Core/Schemas/Cloneable.cs +++ b/src/Squidex.Core/Schemas/CloneableBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// Cloneable.cs +// CloneableBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -10,9 +10,9 @@ using System; namespace Squidex.Core.Schemas { - public abstract class Cloneable + public abstract class CloneableBase { - protected T Clone(Action updater) where T : Cloneable + protected T Clone(Action updater) where T : CloneableBase { var clone = (T)MemberwiseClone(); diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 3a48de351..02a1ee2a0 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -14,6 +14,7 @@ using Microsoft.OData.Edm.Library; using Newtonsoft.Json.Linq; using NJsonSchema; using Squidex.Core.Contents; +using Squidex.Core.Schemas.Validators; using Squidex.Infrastructure; // ReSharper disable InvertIf @@ -22,7 +23,7 @@ using Squidex.Infrastructure; namespace Squidex.Core.Schemas { - public abstract class Field : Cloneable + public abstract class Field : CloneableBase { private readonly Lazy> validators; private readonly long id; diff --git a/src/Squidex.Core/Schemas/FieldProperties.cs b/src/Squidex.Core/Schemas/FieldProperties.cs index 0fbbea7f1..42413b2be 100644 --- a/src/Squidex.Core/Schemas/FieldProperties.cs +++ b/src/Squidex.Core/Schemas/FieldProperties.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Core.Schemas { - public abstract class FieldProperties : NamedElementProperties, IValidatable + public abstract class FieldProperties : NamedElementPropertiesBase, IValidatable { private bool isRequired; private bool isLocalizable; diff --git a/src/Squidex.Core/Schemas/NamedElementProperties.cs b/src/Squidex.Core/Schemas/NamedElementPropertiesBase.cs similarity index 92% rename from src/Squidex.Core/Schemas/NamedElementProperties.cs rename to src/Squidex.Core/Schemas/NamedElementPropertiesBase.cs index 11a9b52aa..ac5d9cf2e 100644 --- a/src/Squidex.Core/Schemas/NamedElementProperties.cs +++ b/src/Squidex.Core/Schemas/NamedElementPropertiesBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// NamedElementProperties.cs +// NamedElementPropertiesBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -10,7 +10,7 @@ using System; namespace Squidex.Core.Schemas { - public abstract class NamedElementProperties + public abstract class NamedElementPropertiesBase { private string label; private string hints; diff --git a/src/Squidex.Core/Schemas/Schema.cs b/src/Squidex.Core/Schemas/Schema.cs index b27427091..8f6f5d61e 100644 --- a/src/Squidex.Core/Schemas/Schema.cs +++ b/src/Squidex.Core/Schemas/Schema.cs @@ -22,7 +22,7 @@ using Squidex.Infrastructure; namespace Squidex.Core.Schemas { - public sealed class Schema : Cloneable + public sealed class Schema : CloneableBase { private readonly string name; private readonly SchemaProperties properties; diff --git a/src/Squidex.Core/Schemas/SchemaProperties.cs b/src/Squidex.Core/Schemas/SchemaProperties.cs index 25ba80c67..16e3a552f 100644 --- a/src/Squidex.Core/Schemas/SchemaProperties.cs +++ b/src/Squidex.Core/Schemas/SchemaProperties.cs @@ -8,7 +8,7 @@ namespace Squidex.Core.Schemas { - public sealed class SchemaProperties : NamedElementProperties + public sealed class SchemaProperties : NamedElementPropertiesBase { } } \ No newline at end of file diff --git a/src/Squidex.Core/Schemas/IValidator.cs b/src/Squidex.Core/Schemas/Validators/IValidator.cs similarity index 92% rename from src/Squidex.Core/Schemas/IValidator.cs rename to src/Squidex.Core/Schemas/Validators/IValidator.cs index 405c0bd01..37be98011 100644 --- a/src/Squidex.Core/Schemas/IValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/IValidator.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Squidex.Core.Schemas +namespace Squidex.Core.Schemas.Validators { public interface IValidator { diff --git a/src/Squidex.Events/SquidexEvent.cs b/src/Squidex.Events/SquidexEvent.cs index e84658bef..3b121958e 100644 --- a/src/Squidex.Events/SquidexEvent.cs +++ b/src/Squidex.Events/SquidexEvent.cs @@ -1,5 +1,5 @@ // ========================================================================== -// SquidexEvent.cs +// SquidexEventBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group diff --git a/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs b/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs index bc9046d87..c5de2f735 100644 --- a/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs @@ -9,6 +9,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using NodaTime; +using System; // ReSharper disable InvertIf @@ -16,26 +17,16 @@ namespace Squidex.Infrastructure.MongoDb { public sealed class InstantSerializer : SerializerBase, IBsonPolymorphicSerializer { - private static bool isRegistered; - private static readonly object LockObject = new object(); + private static readonly Lazy Registerer = new Lazy(() => + { + BsonSerializer.RegisterSerializer(new InstantSerializer()); + + return true; + }); public static bool Register() { - if (!isRegistered) - { - lock (LockObject) - { - if (!isRegistered) - { - BsonSerializer.RegisterSerializer(new InstantSerializer()); - - isRegistered = true; - return true; - } - } - } - - return false; + return !Registerer.IsValueCreated && Registerer.Value; } public bool IsDiscriminatorCompatibleWithObjectSerializer diff --git a/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs index 553a6a728..723e9d2ab 100644 --- a/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; @@ -15,26 +16,16 @@ namespace Squidex.Infrastructure.MongoDb { public class RefTokenSerializer : SerializerBase { - private static bool isRegistered; - private static readonly object LockObject = new object(); + private static readonly Lazy Registerer = new Lazy(() => + { + BsonSerializer.RegisterSerializer(new RefTokenSerializer()); + + return true; + }); public static bool Register() { - if (!isRegistered) - { - lock (LockObject) - { - if (!isRegistered) - { - BsonSerializer.RegisterSerializer(new RefTokenSerializer()); - - isRegistered = true; - return true; - } - } - } - - return false; + return !Registerer.IsValueCreated && Registerer.Value; } public override RefToken Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) diff --git a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs index 4bdd4169d..362d1c01f 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs @@ -8,6 +8,7 @@ using System; using System.Threading.Tasks; +using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Infrastructure.CQRS.Commands { diff --git a/src/Squidex.Infrastructure/CQRS/DomainObject.cs b/src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs similarity index 94% rename from src/Squidex.Infrastructure/CQRS/DomainObject.cs rename to src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs index 14cfd3be5..b8e1ceb30 100644 --- a/src/Squidex.Infrastructure/CQRS/DomainObject.cs +++ b/src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Infrastructure.CQRS { - public abstract class DomainObject : IAggregate, IEquatable + public abstract class DomainObjectBase : IAggregate, IEquatable { private readonly List> uncomittedEvents = new List>(); private readonly Guid id; @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.CQRS get { return id; } } - protected DomainObject(Guid id, int version) + protected DomainObjectBase(Guid id, int version) { Guard.NotEmpty(id, nameof(id)); Guard.GreaterEquals(version, -1, nameof(version)); diff --git a/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs b/src/Squidex.Infrastructure/CQRS/Events/CommonHeaders.cs similarity index 94% rename from src/Squidex.Infrastructure/CQRS/CommonHeaders.cs rename to src/Squidex.Infrastructure/CQRS/Events/CommonHeaders.cs index c48283347..92bf0e3fe 100644 --- a/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/CommonHeaders.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public sealed class CommonHeaders { diff --git a/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs b/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs index 46d0945ea..afd527615 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs @@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.CQRS.Events { public sealed class DefaultMemoryEventNotifier : IEventNotifier { - private readonly string ChannelName = typeof(DefaultMemoryEventNotifier).Name; + private static readonly string ChannelName = typeof(DefaultMemoryEventNotifier).Name; private readonly IPubSub invalidator; diff --git a/src/Squidex.Infrastructure/CQRS/Envelope.cs b/src/Squidex.Infrastructure/CQRS/Events/Envelope.cs similarity index 97% rename from src/Squidex.Infrastructure/CQRS/Envelope.cs rename to src/Squidex.Infrastructure/CQRS/Events/Envelope.cs index 79588727f..e2bf2aa45 100644 --- a/src/Squidex.Infrastructure/CQRS/Envelope.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Envelope.cs @@ -9,7 +9,7 @@ using System; using NodaTime; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public class Envelope where TPayload : class { diff --git a/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeExtensions.cs similarity index 98% rename from src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs rename to src/Squidex.Infrastructure/CQRS/Events/EnvelopeExtensions.cs index 322369def..80886571a 100644 --- a/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeExtensions.cs @@ -10,7 +10,7 @@ using System; using System.Globalization; using NodaTime; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public static class EnvelopeExtensions { diff --git a/src/Squidex.Infrastructure/CQRS/EnvelopeHeaders.cs b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeHeaders.cs similarity index 95% rename from src/Squidex.Infrastructure/CQRS/EnvelopeHeaders.cs rename to src/Squidex.Infrastructure/CQRS/Events/EnvelopeHeaders.cs index 3747ddbbb..f95580f9d 100644 --- a/src/Squidex.Infrastructure/CQRS/EnvelopeHeaders.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeHeaders.cs @@ -5,7 +5,8 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.CQRS + +namespace Squidex.Infrastructure.CQRS.Events { public sealed class EnvelopeHeaders : PropertiesBag { diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs index 80c5ff3ba..aca57463c 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs @@ -18,7 +18,7 @@ using Squidex.Infrastructure.Timers; namespace Squidex.Infrastructure.CQRS.Events { - public sealed class EventReceiver : DisposableObject + public sealed class EventReceiver : DisposableObjectBase { private readonly EventDataFormatter formatter; private readonly IEventStore eventStore; diff --git a/src/Squidex.Infrastructure/DisposableObject.cs b/src/Squidex.Infrastructure/DisposableObjectBase.cs similarity index 94% rename from src/Squidex.Infrastructure/DisposableObject.cs rename to src/Squidex.Infrastructure/DisposableObjectBase.cs index 98da1ace5..a8ead1cbd 100644 --- a/src/Squidex.Infrastructure/DisposableObject.cs +++ b/src/Squidex.Infrastructure/DisposableObjectBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// EnumExtensions.cs +// DisposableObjectBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -10,7 +10,7 @@ using System; namespace Squidex.Infrastructure { - public abstract class DisposableObject : IDisposable + public abstract class DisposableObjectBase : IDisposable { private readonly object disposeLock = new object(); private bool isDisposed; diff --git a/src/Squidex.Infrastructure/Language.cs b/src/Squidex.Infrastructure/Language.cs index 641d9fd82..421ecc491 100644 --- a/src/Squidex.Infrastructure/Language.cs +++ b/src/Squidex.Infrastructure/Language.cs @@ -82,7 +82,7 @@ namespace Squidex.Infrastructure return allLanguages.TryGetValue(iso2Code, out language); } - public static Language TryParse(string input) + public static Language ParseOrNull(string input) { if (string.IsNullOrWhiteSpace(input)) { diff --git a/src/Squidex.Infrastructure/Languages.cs b/src/Squidex.Infrastructure/Languages.cs index 5fb87a0b1..0e03ff9fd 100644 --- a/src/Squidex.Infrastructure/Languages.cs +++ b/src/Squidex.Infrastructure/Languages.cs @@ -5,9 +5,13 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== +// + +using System.CodeDom.Compiler; namespace Squidex.Infrastructure { + [GeneratedCode("LanguagesGenerator", "1.0")] partial class Language { public static readonly Language AA = AddLanguage("aa", "Afar"); diff --git a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs index 94b94b640..b4dc061bf 100644 --- a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs +++ b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.Timers { - public sealed class CompletionTimer : DisposableObject + public sealed class CompletionTimer : DisposableObjectBase { private readonly CancellationTokenSource disposeToken = new CancellationTokenSource(); private readonly Task runTask; diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index de55df60f..606a28e8c 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Squidex.Events; using Squidex.Events.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index e20978138..1599c3f16 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -12,7 +12,6 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Events.Contents; using Squidex.Events.Schemas; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs index 0b57267b1..874764150 100644 --- a/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -13,7 +13,6 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Events; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; using Squidex.Read.History; diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index c72f76f9c..11fc3cc95 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -14,7 +14,6 @@ using Squidex.Events; using Squidex.Events.Schemas; using Squidex.Events.Schemas.Utils; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs index c9830576a..c86311bbc 100644 --- a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs @@ -7,7 +7,7 @@ // ========================================================================== using Squidex.Events; -using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; // ReSharper disable ConvertIfStatementToConditionalTernaryExpression diff --git a/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs b/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs index a3fee940e..4bdd27df8 100644 --- a/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs +++ b/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Events; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; namespace Squidex.Read.MongoDb.Utils diff --git a/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs index a6f0360dc..e561c70e2 100644 --- a/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Squidex.Events.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Read.History; diff --git a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs index 69c3f1493..3537e873b 100644 --- a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs +++ b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs @@ -18,7 +18,7 @@ using Squidex.Read.Utils; namespace Squidex.Read.Apps.Services.Implementations { - public class CachingAppProvider : CachingProvider, IAppProvider + public class CachingAppProvider : CachingProviderBase, IAppProvider { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30); private readonly IAppRepository repository; diff --git a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs index 3dda0acb2..3acb4e287 100644 --- a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs +++ b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs @@ -19,7 +19,7 @@ using Squidex.Read.Utils; namespace Squidex.Read.Contents.Builders { - public sealed class EdmModelBuilder : CachingProvider + public sealed class EdmModelBuilder : CachingProviderBase { public EdmModelBuilder(IMemoryCache cache) : base(cache) diff --git a/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs index 29e9aa32d..3e62195a3 100644 --- a/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs +++ b/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Squidex.Events.Contents; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.History; diff --git a/src/Squidex.Read/History/HistoryEventsCreatorBase.cs b/src/Squidex.Read/History/HistoryEventsCreatorBase.cs index 824a39140..c41c8a7b3 100644 --- a/src/Squidex.Read/History/HistoryEventsCreatorBase.cs +++ b/src/Squidex.Read/History/HistoryEventsCreatorBase.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; // ReSharper disable ConvertIfStatementToReturnStatement diff --git a/src/Squidex.Read/History/IHistoryEventsCreator.cs b/src/Squidex.Read/History/IHistoryEventsCreator.cs index bae86c240..6fafe77cb 100644 --- a/src/Squidex.Read/History/IHistoryEventsCreator.cs +++ b/src/Squidex.Read/History/IHistoryEventsCreator.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Read.History diff --git a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs index dcbb779b7..d7d622956 100644 --- a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs +++ b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using Squidex.Events; using Squidex.Events.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.History; diff --git a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index 85cd97588..d3e857e52 100644 --- a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -19,7 +19,7 @@ using Squidex.Read.Utils; namespace Squidex.Read.Schemas.Services.Implementations { - public class CachingSchemaProvider : CachingProvider, ISchemaProvider + public class CachingSchemaProvider : CachingProviderBase, ISchemaProvider { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private readonly ISchemaRepository repository; diff --git a/src/Squidex.Read/Utils/CachingProvider.cs b/src/Squidex.Read/Utils/CachingProviderBase.cs similarity index 83% rename from src/Squidex.Read/Utils/CachingProvider.cs rename to src/Squidex.Read/Utils/CachingProviderBase.cs index 84f12e4ff..00a3491c6 100644 --- a/src/Squidex.Read/Utils/CachingProvider.cs +++ b/src/Squidex.Read/Utils/CachingProviderBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// CachingProvider.cs +// CachingProviderBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Read.Utils { - public abstract class CachingProvider + public abstract class CachingProviderBase { private readonly IMemoryCache cache; @@ -20,7 +20,7 @@ namespace Squidex.Read.Utils get { return cache; } } - protected CachingProvider(IMemoryCache cache) + protected CachingProviderBase(IMemoryCache cache) { Guard.NotNull(cache, nameof(cache)); diff --git a/src/Squidex.Write/Apps/AppDomainObject.cs b/src/Squidex.Write/Apps/AppDomainObject.cs index c2ee22d62..b1d8a1aae 100644 --- a/src/Squidex.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Write/Apps/AppDomainObject.cs @@ -22,7 +22,7 @@ using Squidex.Write.Apps.Commands; namespace Squidex.Write.Apps { - public class AppDomainObject : DomainObject + public class AppDomainObject : DomainObjectBase { private static readonly Language DefaultLanguage = Language.EN; private readonly AppContributors contributors = new AppContributors(); diff --git a/src/Squidex.Write/Contents/ContentDomainObject.cs b/src/Squidex.Write/Contents/ContentDomainObject.cs index 269fef8ee..f56eda13e 100644 --- a/src/Squidex.Write/Contents/ContentDomainObject.cs +++ b/src/Squidex.Write/Contents/ContentDomainObject.cs @@ -18,7 +18,7 @@ using Squidex.Write.Contents.Commands; namespace Squidex.Write.Contents { - public class ContentDomainObject : DomainObject + public class ContentDomainObject : DomainObjectBase { private bool isDeleted; private bool isCreated; diff --git a/src/Squidex.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Write/Schemas/SchemaDomainObject.cs index edaf8d9f3..bda7fdab9 100644 --- a/src/Squidex.Write/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Write/Schemas/SchemaDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Write.Schemas.Commands; namespace Squidex.Write.Schemas { - public class SchemaDomainObject : DomainObject + public class SchemaDomainObject : DomainObjectBase { private readonly FieldRegistry registry; private bool isDeleted; diff --git a/src/Squidex/Controllers/ContentApi/PingController.cs b/src/Squidex/Controllers/ContentApi/PingController.cs index cc07b3fb8..7f2db9b0e 100644 --- a/src/Squidex/Controllers/ContentApi/PingController.cs +++ b/src/Squidex/Controllers/ContentApi/PingController.cs @@ -16,7 +16,7 @@ namespace Squidex.Controllers.ContentApi [Authorize(Roles = SquidexRoles.AppEditor)] [ApiExceptionFilter] [ServiceFilter(typeof(AppFilterAttribute))] - public class PingController : ControllerBase + public class PingController : Controller { [HttpGet] [Route("ping/{app}/")] diff --git a/src/Squidex/Controllers/ControllerBase.cs b/src/Squidex/Controllers/ControllerBase.cs index ae8eb7f0b..93ba5ef00 100644 --- a/src/Squidex/Controllers/ControllerBase.cs +++ b/src/Squidex/Controllers/ControllerBase.cs @@ -23,11 +23,6 @@ namespace Squidex.Controllers CommandBus = commandBus; } - protected ControllerBase() - { - throw new NotImplementedException(); - } - protected IAppEntity App { get diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index a65042ac5..6c1487a01 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -18,7 +18,7 @@ using Squidex.Controllers.Api; namespace Squidex.Pipeline { - public class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter + public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter { private static readonly List> handlers = new List>(); diff --git a/src/Squidex/Pipeline/AppFilterAttribute.cs b/src/Squidex/Pipeline/AppFilterAttribute.cs index 4c426b520..38fd2a890 100644 --- a/src/Squidex/Pipeline/AppFilterAttribute.cs +++ b/src/Squidex/Pipeline/AppFilterAttribute.cs @@ -22,6 +22,7 @@ using Squidex.Read.Apps.Services; namespace Squidex.Pipeline { + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public sealed class AppFilterAttribute : Attribute, IAsyncAuthorizationFilter { private readonly IAppProvider appProvider; diff --git a/src/Squidex/Pipeline/RandomErrorAttribute.cs b/src/Squidex/Pipeline/RandomErrorAttribute.cs index 93f24ae41..5c56e6f9a 100644 --- a/src/Squidex/Pipeline/RandomErrorAttribute.cs +++ b/src/Squidex/Pipeline/RandomErrorAttribute.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace Squidex.Pipeline { - public class RandomErrorAttribute : ActionFilterAttribute + public sealed class RandomErrorAttribute : ActionFilterAttribute { private static readonly Random random = new Random(); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs index a1278a0b7..ecc495b05 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Infrastructure.CQRS.Commands public long? ExpectedVersion { get; set; } } - private sealed class MyDomainObject : DomainObject + private sealed class MyDomainObject : DomainObjectBase { public MyDomainObject(Guid id, int version) : base(id, version) diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs index e1e579bdb..742b509cc 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs @@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { public class DefaultDomainObjectFactoryTests { - private sealed class DO : DomainObject + private sealed class DO : DomainObjectBase { public DO(Guid id, int version) : base(id, version) { diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs index ffe4340e4..6a4178e1b 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs @@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { } - public sealed class MyDomainObject : DomainObject + public sealed class MyDomainObject : DomainObjectBase { private readonly List appliedEvents = new List(); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectTest.cs b/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectBaseTests.cs similarity index 96% rename from tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectTest.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectBaseTests.cs index c2e87e6ce..32c770f19 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectTest.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectBaseTests.cs @@ -1,5 +1,5 @@ // ========================================================================== -// DomainObjectTest.cs +// DomainObjectBaseTests.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -15,13 +15,13 @@ using System.Linq; namespace Squidex.Infrastructure.CQRS { - public class DomainObjectTest + public class DomainObjectBaseTests { private sealed class MyEvent : IEvent { } - private sealed class DO : DomainObject + private sealed class DO : DomainObjectBase { public DO(Guid id, int version) : base(id, version) diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs index 575039533..bc35d66a6 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.CQRS.Events { private readonly DefaultNameResolver sut = new DefaultNameResolver(); - private sealed class MyUser : DomainObject + private sealed class MyUser : DomainObjectBase { public MyUser(Guid id, int version) : base(id, version) @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.CQRS.Events } } - private sealed class MyUserDomainObject : DomainObject + private sealed class MyUserDomainObject : DomainObjectBase { public MyUserDomainObject(Guid id, int version) : base(id, version) diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeExtensionsTests.cs similarity index 98% rename from tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeExtensionsTests.cs index eb0bc8436..c66c25fe3 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeExtensionsTests.cs @@ -11,7 +11,7 @@ using System.Globalization; using NodaTime; using Xunit; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public class EnvelopeExtensionsTests { diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeHeaderTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeHeaderTests.cs similarity index 97% rename from tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeHeaderTests.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeHeaderTests.cs index c77ad7af8..af879bba2 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeHeaderTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeHeaderTests.cs @@ -9,7 +9,7 @@ using System.Linq; using Xunit; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public class EnvelopeHeaderTests { diff --git a/tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs b/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs similarity index 94% rename from tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs rename to tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs index 2801b87fd..365dc7edb 100644 --- a/tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs +++ b/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs @@ -1,5 +1,5 @@ // ========================================================================== -// DisposableObjectTest.cs +// DisposableObjectBaseTests.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,9 +11,9 @@ using Xunit; namespace Squidex.Infrastructure { - public class DisposableObjectTests + public class DisposableObjectBaseTests { - public sealed class MyDisposableObject : DisposableObject + public sealed class MyDisposableObject : DisposableObjectBase { public int DisposeCallCount { get; set; } diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index be10d78c4..9160ed344 100644 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -74,7 +74,7 @@ namespace Squidex.Infrastructure [InlineData("en ", "en")] public void Should_parse_valid_languages(string input, string languageCode) { - var language = Language.TryParse(input); + var language = Language.ParseOrNull(input); Assert.Equal(language, Language.GetLanguage(languageCode)); } @@ -87,7 +87,7 @@ namespace Squidex.Infrastructure [InlineData(null)] public void Should_parse_invalid_languages(string input) { - var language = Language.TryParse(input); + var language = Language.ParseOrNull(input); Assert.Null(language); } diff --git a/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs b/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs index 27dacb7e1..115ff11ea 100644 --- a/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs +++ b/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Linq; using FluentAssertions; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Write.TestHelpers diff --git a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs index ecb1fdfbd..4679d6a5c 100644 --- a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.CQRS.Commands; namespace Squidex.Write.TestHelpers { - public abstract class HandlerTestBase where T : DomainObject + public abstract class HandlerTestBase where T : DomainObjectBase { private sealed class MockupHandler : IAggregateHandler { diff --git a/tools/GenerateLanguages/Program.cs b/tools/GenerateLanguages/Program.cs index ef474517c..4d25e559b 100644 --- a/tools/GenerateLanguages/Program.cs +++ b/tools/GenerateLanguages/Program.cs @@ -30,9 +30,13 @@ namespace GenerateLanguages writer.WriteLine("// Copyright (c) Squidex Group"); writer.WriteLine("// All rights reserved."); writer.WriteLine("// =========================================================================="); + writer.WriteLine("// "); + writer.WriteLine(); + writer.WriteLine("using System.CodeDom.Compiler;"); writer.WriteLine(); writer.WriteLine("namespace Squidex.Infrastructure"); writer.WriteLine("{"); + writer.WriteLine(" [GeneratedCode(\"LanguagesGenerator\", \"1.0\")]"); writer.WriteLine(" partial class Language"); writer.WriteLine(" {"); From 857af0f0f82b7ffe644bb279517e94c809674e2c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 5 Mar 2017 19:44:31 +0100 Subject: [PATCH 25/66] 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] From e23a928ecb199736f8a23738056ef8de2a46c375 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 6 Mar 2017 22:36:05 +0100 Subject: [PATCH 26/66] Delete button --- src/Squidex.Events/.gitignore | 234 ------------------ .../contents/content-item.component.html | 2 +- .../app/features/schemas/pages/messages.ts | 7 + .../pages/schema/schema-page.component.html | 35 +++ .../pages/schema/schema-page.component.ts | 25 +- .../pages/schemas/schemas-page.component.ts | 16 +- .../shared/services/schemas.service.spec.ts | 14 ++ .../app/shared/services/schemas.service.ts | 7 + src/Squidex/app/theme/_bootstrap.scss | 26 +- 9 files changed, 121 insertions(+), 245 deletions(-) delete mode 100644 src/Squidex.Events/.gitignore diff --git a/src/Squidex.Events/.gitignore b/src/Squidex.Events/.gitignore deleted file mode 100644 index 0ca27f04e..000000000 --- a/src/Squidex.Events/.gitignore +++ /dev/null @@ -1,234 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory -AppPackages/ -BundleArtifacts/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ diff --git a/src/Squidex/app/features/content/pages/contents/content-item.component.html b/src/Squidex/app/features/content/pages/contents/content-item.component.html index 2fcccee7a..39c7edb48 100644 --- a/src/Squidex/app/features/content/pages/contents/content-item.component.html +++ b/src/Squidex/app/features/content/pages/contents/content-item.component.html @@ -22,7 +22,7 @@ Unpublish - + Delete diff --git a/src/Squidex/app/features/schemas/pages/messages.ts b/src/Squidex/app/features/schemas/pages/messages.ts index 03ded7d36..a9ec973ea 100644 --- a/src/Squidex/app/features/schemas/pages/messages.ts +++ b/src/Squidex/app/features/schemas/pages/messages.ts @@ -13,4 +13,11 @@ export class SchemaUpdated { public readonly version: string ) { } +} + +export class SchemaDeleted { + constructor( + public readonly name: string + ) { + } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 0bc795898..f15e775cb 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -12,6 +12,17 @@ Unpublished + +

@@ -66,6 +77,30 @@ + +