diff --git a/src/Squidex.Core/Contents/ContentData.cs b/src/Squidex.Core/Contents/ContentData.cs index 4e655316d..1c8563dad 100644 --- a/src/Squidex.Core/Contents/ContentData.cs +++ b/src/Squidex.Core/Contents/ContentData.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Newtonsoft.Json.Linq; +using Squidex.Core.Schemas; using Squidex.Infrastructure; namespace Squidex.Core.Contents @@ -40,14 +41,71 @@ namespace Squidex.Core.Contents return new ContentData(Fields.Add(fieldName, data)); } - public static ContentData Create(Dictionary> raw) + public static ContentData FromApiRequest(Dictionary> request) { - return new ContentData(raw.ToImmutableDictionary(x => x.Key, x => new ContentFieldData(x.Value.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)), StringComparer.OrdinalIgnoreCase)); + Guard.NotNull(request, nameof(request)); + + return new ContentData(request.ToImmutableDictionary(x => x.Key, x => new ContentFieldData(x.Value.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)), StringComparer.OrdinalIgnoreCase)); } - public Dictionary> ToRaw() + public Dictionary> ToApiResponse(Schema schema, IReadOnlyCollection languages, Language masterLanguage) { - return fields.ToDictionary(x => x.Key, x => x.Value.ValueByLanguage.ToDictionary(y => y.Key, y => y.Value)); + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(languages, nameof(languages)); + Guard.NotNull(masterLanguage, nameof(masterLanguage)); + + var invariantCode = Language.Invariant.Iso2Code; + + var result = new Dictionary>(); + + foreach (var fieldValue in fields) + { + Field field; + + if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out field)) + { + continue; + } + + var fieldResult = new Dictionary(); + var fieldValues = fieldValue.Value.ValueByLanguage; + + if (field.RawProperties.IsLocalizable) + { + foreach (var language in languages) + { + var languageCode = language.Iso2Code; + + JToken value; + + if (fieldValues.TryGetValue(languageCode, out value)) + { + fieldResult.Add(languageCode, value); + } + } + } + else + { + JToken value; + + if (fieldValues.TryGetValue(invariantCode, out value)) + { + fieldResult.Add(invariantCode, value); + } + else if (fieldValues.TryGetValue(masterLanguage.Iso2Code, out value)) + { + fieldResult.Add(invariantCode, value); + } + else if (fieldValues.Count > 0) + { + fieldResult.Add(invariantCode, fieldValues.Values.First()); + } + } + + result.Add(field.Name, fieldResult); + } + + return result; } } } diff --git a/src/Squidex.Core/Schemas/Schema.cs b/src/Squidex.Core/Schemas/Schema.cs index 09cb2cb57..455a4ac89 100644 --- a/src/Squidex.Core/Schemas/Schema.cs +++ b/src/Squidex.Core/Schemas/Schema.cs @@ -44,6 +44,11 @@ namespace Squidex.Core.Schemas get { return fieldsById; } } + public ImmutableDictionary FieldsByName + { + get { return fieldsByName; } + } + public SchemaProperties Properties { get { return properties; } diff --git a/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs index b9964386e..1ff9721dc 100644 --- a/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs @@ -37,19 +37,19 @@ namespace Squidex.Store.MongoDb.Apps [BsonElement] public Dictionary Contributors { get; set; } - IEnumerable IAppEntity.Clients + IReadOnlyCollection IAppEntity.Clients { get { return Clients.Values; } } - IEnumerable IAppEntity.Contributors + IReadOnlyCollection IAppEntity.Contributors { get { return Contributors.Values; } } - IEnumerable IAppEntity.Languages + IReadOnlyCollection IAppEntity.Languages { - get { return Languages.Select(Language.GetLanguage); } + get { return Languages.Select(Language.GetLanguage).ToList(); } } Language IAppEntity.MasterLanguage diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 5e7ab5815..bd141fb3b 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -65,7 +65,7 @@ namespace Squidex.Controllers.ContentApi if (x.Data != null) { - itemModel.Data = x.Data.ToRaw(); + itemModel.Data = x.Data.ToApiResponse(schemaEntity.Schema, App.Languages, App.MasterLanguage); } return itemModel; @@ -97,7 +97,7 @@ namespace Squidex.Controllers.ContentApi if (content.Data != null) { - model.Data = content.Data.ToRaw(); + model.Data = content.Data.ToApiResponse(schemaEntity.Schema, App.Languages, App.MasterLanguage); } return Ok(model); @@ -107,7 +107,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/")] public async Task PostContent([FromBody] Dictionary> request) { - var command = new CreateContent { Data = ContentData.Create(request), AggregateId = Guid.NewGuid() }; + var command = new CreateContent { Data = ContentData.FromApiRequest(request), AggregateId = Guid.NewGuid() }; var context = await CommandBus.PublishAsync(command); var result = context.Result(); @@ -119,7 +119,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/{id}")] public async Task PutContent(Guid id, [FromBody] Dictionary> request) { - var command = new UpdateContent { AggregateId = id, Data = ContentData.Create(request) }; + var command = new UpdateContent { AggregateId = id, Data = ContentData.FromApiRequest(request) }; await CommandBus.PublishAsync(command); diff --git a/src/Squidex/Controllers/ControllerBase.cs b/src/Squidex/Controllers/ControllerBase.cs index d29906722..bbab80bff 100644 --- a/src/Squidex/Controllers/ControllerBase.cs +++ b/src/Squidex/Controllers/ControllerBase.cs @@ -10,6 +10,7 @@ using System; using Microsoft.AspNetCore.Mvc; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline; +using Squidex.Read.Apps; namespace Squidex.Controllers { @@ -27,7 +28,7 @@ namespace Squidex.Controllers throw new NotImplementedException(); } - public Guid AppId + public IAppEntity App { get { @@ -38,7 +39,15 @@ namespace Squidex.Controllers throw new InvalidOperationException("Not in a app context"); } - return appFeature.App.Id; + return appFeature.App; + } + } + + public Guid AppId + { + get + { + return App.Id; } } } 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 984f3fb51..d074eba15 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 @@ -86,7 +86,7 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, const oldSchema = schemas.find(i => i.name === message.name); if (oldSchema) { - const me = `subject:${this.authService.user.id}`; + const me = `subject:${this.authService.user!.id}`; const newSchema = new SchemaDto( diff --git a/src/Squidex/app/framework/angular/control-errors.component.ts b/src/Squidex/app/framework/angular/control-errors.component.ts index 654127448..8166053cc 100644 --- a/src/Squidex/app/framework/angular/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/control-errors.component.ts @@ -46,7 +46,7 @@ export class ControlErrorsComponent implements OnChanges, OnInit { @Input() public submitted: boolean; - public get errorMessages(): string[] { + public get errorMessages(): string[] | null { if (!this.control) { return null; } diff --git a/src/Squidex/app/framework/angular/copy.directive.ts b/src/Squidex/app/framework/angular/copy.directive.ts index 37c6f1bfc..494693c3b 100644 --- a/src/Squidex/app/framework/angular/copy.directive.ts +++ b/src/Squidex/app/framework/angular/copy.directive.ts @@ -28,7 +28,10 @@ export class CopyDirective { const prevSelectionEnd = element.selectionEnd; element.focus(); - element.setSelectionRange(0, element.value.length); + + if (element instanceof HTMLInputElement) { + element.setSelectionRange(0, element.value.length); + } try { document.execCommand('copy'); @@ -40,6 +43,8 @@ export class CopyDirective { currentFocus.focus(); } - element.setSelectionRange(prevSelectionStart, prevSelectionEnd); + if (element instanceof HTMLInputElement) { + element.setSelectionRange(prevSelectionStart, prevSelectionEnd); + } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/http-utils.ts b/src/Squidex/app/framework/angular/http-utils.ts index 309b48d0f..eeffc541c 100644 --- a/src/Squidex/app/framework/angular/http-utils.ts +++ b/src/Squidex/app/framework/angular/http-utils.ts @@ -10,7 +10,7 @@ import { Observable } from 'rxjs'; export class EntityCreatedDto { constructor( - public readonly id: string + public readonly id: any ) { } } diff --git a/src/Squidex/app/framework/angular/modal-view.directive.ts b/src/Squidex/app/framework/angular/modal-view.directive.ts index e2857422f..bfdd2830a 100644 --- a/src/Squidex/app/framework/angular/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modal-view.directive.ts @@ -62,10 +62,7 @@ export class ModalViewDirective implements OnChanges, OnInit, OnDestroy { } public ngOnChanges() { - if (this.subscription) { - this.subscription.unsubscribe(); - this.subscription = null; - } + this.subscription.unsubscribe(); if (this.modalView) { this.subscription = this.modalView.isOpen.subscribe(isOpen => { diff --git a/src/Squidex/app/framework/angular/validators.spec.ts b/src/Squidex/app/framework/angular/validators.spec.ts index 4b0b3f0d1..e0da40d33 100644 --- a/src/Squidex/app/framework/angular/validators.spec.ts +++ b/src/Squidex/app/framework/angular/validators.spec.ts @@ -9,31 +9,33 @@ import { FormControl, Validators } from '@angular/forms'; import { ValidatorsEx } from './../'; -describe('Validators', () => { - let validateBetween: any; - - beforeEach(() => { - validateBetween = ValidatorsEx.between(10, 200); - }); - +describe('ValidatorsEx.between', () => { it('should return null validator if no min value or max value', () => { const validator = ValidatorsEx.between(undefined, undefined); expect(validator).toBe(Validators.nullValidator); }); + it('should return empty value when value is valid', () => { + const input = new FormControl(4); + + const error = ValidatorsEx.between(1, 5)(input); + + expect(error).toEqual({}); + }); + it('should return error when not a number', () => { const input = new FormControl('text'); - const error = validateBetween(input); + const error = ValidatorsEx.between(1, 5)(input); expect(error.validnumber).toBeFalsy(); }); it('should return error if less than minimum setting', () => { - const input = new FormControl(5); + const input = new FormControl(-5); - const error = validateBetween(input); + const error = ValidatorsEx.between(1, 5)(input); expect(error.minvalue).toBeDefined(); }); @@ -41,17 +43,17 @@ describe('Validators', () => { it('should return error if greater than maximum setting', () => { const input = new FormControl(300); - const error = validateBetween(input); + const error = ValidatorsEx.between(1, 5)(input); expect(error.maxvalue).toBeDefined(); }); +}); - it('should return empty value when value is valid', () => { - const input = new FormControl(50); - - const error = validateBetween(input); +describe('ValidatorsEx.validValues', () => { + it('should return null validator if values not defined', () => { + const validator = ValidatorsEx.validValues(null!); - expect(error).toEqual({}); + expect(validator).toBe(Validators.nullValidator); }); it('should return empty value if value is in allowed values', () => { @@ -70,3 +72,87 @@ describe('Validators', () => { expect(error.validvalues).toBeDefined(); }); }); + +describe('ValidatorsEx.pattern', () => { + it('should return null validator if pattern not defined', () => { + const validator = ValidatorsEx.pattern(undefined!, undefined); + + expect(validator).toBe(Validators.nullValidator); + }); + + it('should return empty value when value is valid pattern', () => { + const input = new FormControl('1234'); + + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + + expect(error).toEqual({}); + }); + + it('should return empty value when value is null string', () => { + const input = new FormControl(null); + + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + + expect(error).toEqual({}); + }); + + it('should return empty value when value is empty string', () => { + const input = new FormControl(''); + + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + + expect(error).toEqual({}); + }); + + it('should return error with message if value does not match pattern string', () => { + const input = new FormControl('abc'); + + const error = ValidatorsEx.pattern('[0-9]{1,4}', 'My-Message')(input); + const expected: any = { + patternmessage: { + requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc', message: 'My-Message' + } + }; + + expect(error).toEqual(expected); + }); + + it('should return error with message if value does not match pattern', () => { + const input = new FormControl('abc'); + + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/, 'My-Message')(input); + const expected: any = { + patternmessage: { + requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc', message: 'My-Message' + } + }; + + expect(error).toEqual(expected); + }); + + it('should return error without message if value does not match pattern string', () => { + const input = new FormControl('abc'); + + const error = ValidatorsEx.pattern('[0-9]{1,4}')(input); + const expected: any = { + pattern: { + requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc' + } + }; + + expect(error).toEqual(expected); + }); + + it('should return error without message if value does not match pattern', () => { + const input = new FormControl('abc'); + + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + const expected: any = { + pattern: { + requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc' + } + }; + + expect(error).toEqual(expected); + }); +}); diff --git a/src/Squidex/app/framework/angular/validators.ts b/src/Squidex/app/framework/angular/validators.ts index 3d75936e5..277a31ce3 100644 --- a/src/Squidex/app/framework/angular/validators.ts +++ b/src/Squidex/app/framework/angular/validators.ts @@ -11,8 +11,8 @@ import { Validators } from '@angular/forms'; -export class ValidatorsEx { - public static pattern(pattern: string | RegExp, message: string | undefined = undefined): ValidatorFn { +export module ValidatorsEx { + export function pattern(pattern: string | RegExp, message?: string): ValidatorFn { if (!pattern) { return Validators.nullValidator; } @@ -32,7 +32,7 @@ export class ValidatorsEx { const n: string = control.value; if (n == null || n.length === 0) { - return null; + return {}; } if (!regex.test(n)) { @@ -43,11 +43,11 @@ export class ValidatorsEx { } } - return null; + return {}; }; } - public static between(minValue: number | undefined, maxValue: number | undefined) { + export function between(minValue?: number, maxValue?: number) { if (!minValue || !maxValue) { return Validators.nullValidator; } @@ -67,7 +67,11 @@ export class ValidatorsEx { }; } - public static validValues(values: T[]) { + export function validValues(values: T[]) { + if (!values) { + return Validators.nullValidator; + } + return (control: AbstractControl): { [key: string]: any } => { const n: T = control.value; diff --git a/src/Squidex/app/framework/services/notification.service.spec.ts b/src/Squidex/app/framework/services/notification.service.spec.ts index 4b223d9a4..45d5c1c03 100644 --- a/src/Squidex/app/framework/services/notification.service.spec.ts +++ b/src/Squidex/app/framework/services/notification.service.spec.ts @@ -44,7 +44,7 @@ describe('NotificationService', () => { const notificationService = new NotificationService(); const notification = Notification.error('Message'); - let publishedNotification: Notification; + let publishedNotification: Notification | null = null; notificationService.notifications.subscribe(result => { publishedNotification = result; diff --git a/src/Squidex/app/framework/services/title.service.spec.ts b/src/Squidex/app/framework/services/title.service.spec.ts index 76a9034e4..db05a5b1a 100644 --- a/src/Squidex/app/framework/services/title.service.spec.ts +++ b/src/Squidex/app/framework/services/title.service.spec.ts @@ -45,7 +45,7 @@ describe('TitleService', () => { }); it('should append suffix to title', () => { - const titleService = new TitleService(new TitlesConfig({}, null, 'myapp')); + const titleService = new TitleService(new TitlesConfig({}, undefined, 'myapp')); titleService.setTitle('my-title', {}); @@ -53,9 +53,9 @@ describe('TitleService', () => { }); it('should do nothing if title is null', () => { - const titleService = new TitleService(new TitlesConfig({}, null, 'myapp')); + const titleService = new TitleService(new TitlesConfig({}, undefined, 'myapp')); - titleService.setTitle(null, {}); + titleService.setTitle(null!, {}); expect(document.title).toBe(''); }); diff --git a/src/Squidex/app/framework/services/title.service.ts b/src/Squidex/app/framework/services/title.service.ts index d44219c9b..2c5cd751f 100644 --- a/src/Squidex/app/framework/services/title.service.ts +++ b/src/Squidex/app/framework/services/title.service.ts @@ -10,8 +10,8 @@ import { Injectable } from '@angular/core'; export class TitlesConfig { constructor( public readonly value: { [key: string]: string }, - public readonly prefix: string = null, - public readonly suffix: string = null + public readonly prefix?: string, + public readonly suffix?: string ) { } } diff --git a/src/Squidex/app/framework/utils/string-helper.spec.ts b/src/Squidex/app/framework/utils/string-helper.spec.ts index 87bcf49eb..476491555 100644 --- a/src/Squidex/app/framework/utils/string-helper.spec.ts +++ b/src/Squidex/app/framework/utils/string-helper.spec.ts @@ -10,13 +10,13 @@ import { StringHelper } from './../'; describe('StringHelper', () => { it('should return empty text if value is null or undefined', () => { - expect(StringHelper.firstNonEmpty(null)).toBe(''); - expect(StringHelper.firstNonEmpty(undefined)).toBe(''); + expect(StringHelper.firstNonEmpty(null!)).toBe(''); + expect(StringHelper.firstNonEmpty(undefined!)).toBe(''); }); it('should return fallback name if label is undefined or null', () => { - expect(StringHelper.firstNonEmpty(null, 'fallback')).toBe('fallback'); - expect(StringHelper.firstNonEmpty(undefined, 'fallback')).toBe('fallback'); + expect(StringHelper.firstNonEmpty(null!, 'fallback')).toBe('fallback'); + expect(StringHelper.firstNonEmpty(undefined!, 'fallback')).toBe('fallback'); }); it('should return label if value is valid', () => { @@ -36,6 +36,6 @@ describe('StringHelper', () => { }); it('should return empty string if also fallback not found', () => { - expect(StringHelper.firstNonEmpty(null, undefined, '')).toBe(''); + expect(StringHelper.firstNonEmpty(null!, undefined!, '')).toBe(''); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts b/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts index dbc130821..d29cb529d 100644 --- a/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts +++ b/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts @@ -27,7 +27,7 @@ describe('AppMustExistGuard', () => { const guard = new AppMustExistGuard(appsStore.object, router); - guard.canActivate(route, null) + guard.canActivate(route, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -44,7 +44,7 @@ describe('AppMustExistGuard', () => { const guard = new AppMustExistGuard(appsStore.object, router); - guard.canActivate(route, null) + guard.canActivate(route, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -61,7 +61,7 @@ describe('AppMustExistGuard', () => { const guard = new AppMustExistGuard(appsStore.object, router); - guard.canActivate(route, null) + guard.canActivate(route, {}) .then(result => { expect(result).toBeTruthy(); expect(router.lastNavigation).toBeUndefined(); diff --git a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts b/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts index 09adec8f2..4a98b996f 100644 --- a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts +++ b/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts @@ -26,7 +26,7 @@ describe('MustBeAuthenticatedGuard', () => { const guard = new MustBeAuthenticatedGuard(authService.object, router); - guard.canActivate(null, null) + guard.canActivate({}, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['']); @@ -42,7 +42,7 @@ describe('MustBeAuthenticatedGuard', () => { const guard = new MustBeAuthenticatedGuard(authService.object, router); - guard.canActivate(null, null) + guard.canActivate({}, {}) .then(result => { expect(result).toBeTruthy(); expect(router.lastNavigation).toBeUndefined(); diff --git a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts b/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts index 26991e758..79ecce5e7 100644 --- a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts +++ b/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts @@ -26,7 +26,7 @@ describe('MustBeNotAuthenticatedGuard', () => { const guard = new MustBeNotAuthenticatedGuard(authService.object, router); - guard.canActivate(null, null) + guard.canActivate({}, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['app']); @@ -42,7 +42,7 @@ describe('MustBeNotAuthenticatedGuard', () => { const guard = new MustBeNotAuthenticatedGuard(authService.object, router); - guard.canActivate(null, null) + guard.canActivate({}, {}) .then(result => { expect(result).toBeTruthy(); expect(router.lastNavigation).toBeUndefined(); 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 ea7d85a63..53fc46a9b 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 @@ -13,7 +13,7 @@ import { SchemasService } from 'shared'; import { ResolvePublishedSchemaGuard } from './resolve-published-schema.guard'; import { RouterMockup } from './router-mockup'; -describe('ResolveSchemaGuard', () => { +describe('ResolvePublishedSchemaGuard', () => { const route = { params: { appName: 'my-app' @@ -31,14 +31,20 @@ describe('ResolveSchemaGuard', () => { appsStore = Mock.ofType(SchemasService); }); + it('should throw if route does not contain parameter', () => { + const guard = new ResolvePublishedSchemaGuard(appsStore.object, new RouterMockup()); + + expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app and schema name.'); + }); + it('should navigate to 404 page if schema is not found', (done) => { appsStore.setup(x => x.getSchema('my-app', 'my-schema')) - .returns(() => Observable.of(null)); + .returns(() => Observable.of(null!)); const router = new RouterMockup(); const guard = new ResolvePublishedSchemaGuard(appsStore.object, router); - guard.resolve(route, null) + guard.resolve(route, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -54,7 +60,7 @@ describe('ResolveSchemaGuard', () => { const guard = new ResolvePublishedSchemaGuard(appsStore.object, router); - guard.resolve(route, null) + guard.resolve(route, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -72,7 +78,7 @@ describe('ResolveSchemaGuard', () => { const guard = new ResolvePublishedSchemaGuard(appsStore.object, router); - guard.resolve(route, null) + guard.resolve(route, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -90,7 +96,7 @@ describe('ResolveSchemaGuard', () => { const guard = new ResolvePublishedSchemaGuard(appsStore.object, router); - guard.resolve(route, null) + guard.resolve(route, {}) .then(result => { expect(result).toBe(schema); 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 bd19dc7ff..f01c1d866 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts @@ -22,6 +22,10 @@ export class ResolvePublishedSchemaGuard implements Resolve { const appName = this.findParameter(route, 'appName'); const schemaName = this.findParameter(route, 'schemaName'); + if (!appName || !schemaName) { + throw 'Route must contain app and schema name.'; + } + const result = this.schemasService.getSchema(appName, schemaName).toPromise() .then(dto => { @@ -41,8 +45,8 @@ export class ResolvePublishedSchemaGuard implements Resolve { return result; } - private findParameter(route: ActivatedRouteSnapshot, name: string) { - let result: string; + private findParameter(route: ActivatedRouteSnapshot, name: string): string | null { + let result: string | null = null; while (route) { result = route.params[name]; 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 b95ffef99..49d0e74b3 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts @@ -31,14 +31,20 @@ describe('ResolveSchemaGuard', () => { appsStore = Mock.ofType(SchemasService); }); + it('should throw if route does not contain parameter', () => { + const guard = new ResolveSchemaGuard(appsStore.object, new RouterMockup()); + + expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app and schema name.'); + }); + it('should navigate to 404 page if schema is not found', (done) => { appsStore.setup(x => x.getSchema('my-app', 'my-schema')) - .returns(() => Observable.of(null)); + .returns(() => Observable.of(null!)); const router = new RouterMockup(); const guard = new ResolveSchemaGuard(appsStore.object, router); - guard.resolve(route, null) + guard.resolve(route, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -49,12 +55,12 @@ describe('ResolveSchemaGuard', () => { it('should navigate to 404 page if schema loading fails', (done) => { appsStore.setup(x => x.getSchema('my-app', 'my-schema')) - .returns(() => Observable.throw(null)); + .returns(() => Observable.throw(null!)); const router = new RouterMockup(); const guard = new ResolveSchemaGuard(appsStore.object, router); - guard.resolve(route, null) + guard.resolve(route, {}) .then(result => { expect(result).toBeFalsy(); expect(router.lastNavigation).toEqual(['/404']); @@ -72,7 +78,7 @@ describe('ResolveSchemaGuard', () => { const guard = new ResolveSchemaGuard(appsStore.object, router); - guard.resolve(route, null) + guard.resolve(route, {}) .then(result => { expect(result).toBe(schema); diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.ts index 81e5ef117..d365c659f 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.ts @@ -22,23 +22,31 @@ export class ResolveSchemaGuard implements Resolve { const appName = this.findParameter(route, 'appName'); const schemaName = this.findParameter(route, 'schemaName'); + if (!appName || !schemaName) { + throw 'Route must contain app and schema name.'; + } + const result = this.schemasService.getSchema(appName, schemaName).toPromise() .then(dto => { if (!dto) { this.router.navigate(['/404']); + + return null; } return dto; }).catch(() => { this.router.navigate(['/404']); + + return null; }); return result; } - private findParameter(route: ActivatedRouteSnapshot, name: string) { - let result: string; + private findParameter(route: ActivatedRouteSnapshot, name: string): string | null { + let result: string | null = null; while (route) { result = route.params[name]; 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 0068f24ee..41a0db76d 100644 --- a/src/Squidex/app/shared/services/app-clients.service.spec.ts +++ b/src/Squidex/app/shared/services/app-clients.service.spec.ts @@ -53,7 +53,7 @@ describe('AppClientsService', () => { )) .verifiable(Times.once()); - let clients: AppClientDto[] = null; + let clients: AppClientDto[] | null = null; appClientsService.getClients('my-app').subscribe(result => { clients = result; @@ -71,7 +71,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', It.isValue(dto))) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', dto)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -86,7 +86,7 @@ describe('AppClientsService', () => { )) .verifiable(Times.once()); - let client: AppClientDto = null; + let client: AppClientDto | null = null; appClientsService.postClient('my-app', dto).subscribe(result => { client = result; @@ -101,7 +101,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', It.isValue(dto))) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', dto)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -131,7 +131,7 @@ describe('AppClientsService', () => { it('should make form request to create token', () => { const body = 'grant_type=client_credentials&scope=squidex-api&client_id=my-app:myClientId&client_secret=mySecret'; - http.setup(x => x.post('http://service/p/identity-server/connect/token', It.isValue(body), It.isAny())) + http.setup(x => x.post('http://service/p/identity-server/connect/token', body, It.isAny())) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -143,9 +143,9 @@ describe('AppClientsService', () => { )) .verifiable(Times.once()); - let accessTokenDto: AccessTokenDto = null; + let accessTokenDto: AccessTokenDto | null = null; - appClientsService.createToken('my-app', new AppClientDto('myClientId', null, 'mySecret', null)).subscribe(result => { + appClientsService.createToken('my-app', new AppClientDto('myClientId', 'myClient', 'mySecret', DateTime.now())).subscribe(result => { accessTokenDto = result; }); 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 bfb49ee21..4867857ae 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.spec.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.spec.ts @@ -7,7 +7,7 @@ import { Response, ResponseOptions } from '@angular/http'; import { Observable } from 'rxjs'; -import { It, IMock, Mock, Times } from 'typemoq'; +import { IMock, Mock, Times } from 'typemoq'; import { ApiUrlConfig, @@ -42,7 +42,7 @@ describe('AppContributorsService', () => { )) .verifiable(Times.once()); - let contributors: AppContributorDto[] = null; + let contributors: AppContributorDto[] | null = null; appContributorsService.getContributors('my-app').subscribe(result => { contributors = result; @@ -58,9 +58,9 @@ describe('AppContributorsService', () => { }); it('should make post request to assign contributor', () => { - const contributor = new AppContributorDto('123', 'Owner'); + const dto = new AppContributorDto('123', 'Owner'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', It.isValue(contributor))) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', dto)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -68,7 +68,7 @@ describe('AppContributorsService', () => { )) .verifiable(Times.once()); - appContributorsService.postContributor('my-app', contributor); + appContributorsService.postContributor('my-app', dto); authService.verifyAll(); }); 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 e331bab29..617201ac3 100644 --- a/src/Squidex/app/shared/services/app-languages.service.spec.ts +++ b/src/Squidex/app/shared/services/app-languages.service.spec.ts @@ -7,7 +7,7 @@ import { Response, ResponseOptions } from '@angular/http'; import { Observable } from 'rxjs'; -import { It, IMock, Mock, Times } from 'typemoq'; +import { IMock, Mock, Times } from 'typemoq'; import { AddAppLanguageDto, @@ -45,7 +45,7 @@ describe('AppLanguagesService', () => { )) .verifiable(Times.once()); - let languages: AppLanguageDto[] = null; + let languages: AppLanguageDto[] | null = null; appLanguagesService.getLanguages('my-app').subscribe(result => { languages = result; @@ -63,7 +63,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', It.isValue(dto))) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', dto)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -76,7 +76,7 @@ describe('AppLanguagesService', () => { )) .verifiable(Times.once()); - let language: AppLanguageDto; + let language: AppLanguageDto | null = null; appLanguagesService.postLanguages('my-app', dto).subscribe(result => { language = result; @@ -91,7 +91,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', It.isValue(dto))) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto)) .returns(() => Observable.of( new Response( new ResponseOptions() diff --git a/src/Squidex/app/shared/services/apps-store.service.spec.ts b/src/Squidex/app/shared/services/apps-store.service.spec.ts index 0f2878fb5..964330b70 100644 --- a/src/Squidex/app/shared/services/apps-store.service.spec.ts +++ b/src/Squidex/app/shared/services/apps-store.service.spec.ts @@ -42,8 +42,8 @@ describe('AppsStoreService', () => { const store = new AppsStoreService(authService.object, appsService.object); - let result1: AppDto[]; - let result2: AppDto[]; + let result1: AppDto[] | null = null; + let result2: AppDto[] | null = null; store.apps.subscribe(x => { result1 = x; @@ -70,8 +70,8 @@ describe('AppsStoreService', () => { const store = new AppsStoreService(authService.object, appsService.object); - let result1: AppDto[]; - let result2: AppDto[]; + let result1: AppDto[] | null = null; + let result2: AppDto[] | null = null; store.apps.subscribe(x => { result1 = x; @@ -104,8 +104,8 @@ describe('AppsStoreService', () => { const store = new AppsStoreService(authService.object, appsService.object); - let result1: AppDto[]; - let result2: AppDto[]; + let result1: AppDto[] | null = null; + let result2: AppDto[] | null = null; store.apps.subscribe(x => { result1 = x; @@ -134,7 +134,7 @@ describe('AppsStoreService', () => { const store = new AppsStoreService(authService.object, appsService.object); - let result: AppDto[] = null; + let result: AppDto[] | null = null; store.createApp(new CreateAppDto('new-name'), now).subscribe(x => { /* Do Nothing */ }); diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/src/Squidex/app/shared/services/apps.service.spec.ts index 37216fcfe..a2fdfde4e 100644 --- a/src/Squidex/app/shared/services/apps.service.spec.ts +++ b/src/Squidex/app/shared/services/apps.service.spec.ts @@ -7,7 +7,7 @@ import { Response, ResponseOptions } from '@angular/http'; import { Observable } from 'rxjs'; -import { It, IMock, Mock, Times } from 'typemoq'; +import { IMock, Mock, Times } from 'typemoq'; import { ApiUrlConfig, @@ -51,7 +51,7 @@ describe('AppsService', () => { )) .verifiable(Times.once()); - let apps: AppDto[] = null; + let apps: AppDto[] | null = null; appsService.getApps().subscribe(result => { apps = result; @@ -66,9 +66,9 @@ describe('AppsService', () => { }); it('should make post request to create app', () => { - const createApp = new CreateAppDto('new-app'); + const dto = new CreateAppDto('new-app'); - authService.setup(x => x.authPost('http://service/p/api/apps', It.isValue(createApp))) + authService.setup(x => x.authPost('http://service/p/api/apps', dto)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -80,13 +80,13 @@ describe('AppsService', () => { )) .verifiable(Times.once()); - let newCreated: EntityCreatedDto = null; + let created: EntityCreatedDto | null = null; - appsService.postApp(createApp).subscribe(result => { - newCreated = result; + appsService.postApp(dto).subscribe(result => { + created = result; }).unsubscribe(); - expect(newCreated).toEqual(new EntityCreatedDto('123')); + expect(created).toEqual(new EntityCreatedDto('123')); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index abf7cdd5c..d5b806949 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -7,7 +7,7 @@ import { Response, ResponseOptions } from '@angular/http'; import { Observable } from 'rxjs'; -import { It, IMock, Mock, Times } from 'typemoq'; +import { IMock, Mock, Times } from 'typemoq'; import { ApiUrlConfig, @@ -27,7 +27,7 @@ describe('ContentsService', () => { contentsService = new ContentsService(authService.object, new ApiUrlConfig('http://service/p/')); }); - it('should make get request to get content1', () => { + it('should make get request to get contents', () => { authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema?take=17&skip=13&query=my-query&nonPublished=true')) .returns(() => Observable.of( new Response( @@ -54,7 +54,7 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - let contents: ContentDto[] = null; + let contents: ContentDto[] | null = null; contentsService.getContents('my-app', 'my-schema', 17, 13, 'my-query').subscribe(result => { contents = result; @@ -87,7 +87,7 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - let content: ContentDto = null; + let content: ContentDto | null = null; contentsService.getContent('my-app', 'my-schema', 'content1').subscribe(result => { content = result; @@ -102,7 +102,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', It.isValue(dto))) + authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -114,7 +114,7 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - let created: EntityCreatedDto = null; + let created: EntityCreatedDto | null = null; contentsService.postContent('my-app', 'my-schema', dto).subscribe(result => { created = result; @@ -129,7 +129,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', It.isValue(dto))) + authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1', dto)) .returns(() => Observable.of( new Response( new ResponseOptions() diff --git a/src/Squidex/app/shared/services/history.service.spec.ts b/src/Squidex/app/shared/services/history.service.spec.ts index 7b3987da3..82c1e1e5b 100644 --- a/src/Squidex/app/shared/services/history.service.spec.ts +++ b/src/Squidex/app/shared/services/history.service.spec.ts @@ -48,7 +48,7 @@ describe('HistoryService', () => { )) .verifiable(Times.once()); - let events: HistoryEventDto[] = null; + let events: HistoryEventDto[] | null = null; languageService.getHistory('my-app', 'settings.contributors').subscribe(result => { events = result; diff --git a/src/Squidex/app/shared/services/languages.service.spec.ts b/src/Squidex/app/shared/services/languages.service.spec.ts index 8e53a768f..8414f920c 100644 --- a/src/Squidex/app/shared/services/languages.service.spec.ts +++ b/src/Squidex/app/shared/services/languages.service.spec.ts @@ -42,7 +42,7 @@ describe('LanguageService', () => { )) .verifiable(Times.once()); - let languages: LanguageDto[] = null; + let languages: LanguageDto[] | null = null; languageService.getLanguages().subscribe(result => { languages = result; diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts new file mode 100644 index 000000000..e31b68374 --- /dev/null +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -0,0 +1,334 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Response, ResponseOptions } from '@angular/http'; +import { Observable } from 'rxjs'; +import { It, IMock, Mock, Times } from 'typemoq'; + +import { + AddFieldDto, + ApiUrlConfig, + AuthService, + CreateSchemaDto, + createProperties, + DateTime, + EntityCreatedDto, + FieldDto, + SchemaDetailsDto, + SchemaDto, + SchemasService, + UpdateFieldDto, + UpdateSchemaDto +} from './../'; + +describe('SchemasService', () => { + let authService: IMock; + let schemasService: SchemasService; + + beforeEach(() => { + authService = Mock.ofType(AuthService); + schemasService = new SchemasService(authService.object, new ApiUrlConfig('http://service/p/')); + }); + + it('should throw if creating invalid property type', () => { + expect(() => createProperties('invalid')).toThrow('Invalid properties type'); + }); + + it('should make get request to get schemas', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/schemas')) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: [{ + id: 'id1', + name: 'name1', + label: 'label1', + isPublished: true, + created: '2016-12-12T10:10', + createdBy: 'Created1', + lastModified: '2017-12-12T10:10', + lastModifiedBy: 'LastModifiedBy1', + data: {} + }, { + id: 'id2', + name: 'name2', + label: 'label2', + isPublished: true, + created: '2016-10-12T10:10', + createdBy: 'Created2', + lastModified: '2017-10-12T10:10', + lastModifiedBy: 'LastModifiedBy2', + data: {} + }] + }) + ) + )) + .verifiable(Times.once()); + + let schemas: SchemaDto[] | null = null; + + schemasService.getSchemas('my-app').subscribe(result => { + schemas = result; + }).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')) + ]); + + authService.verifyAll(); + }); + + it('should make get request to get schema', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/schemas/my-schema')) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: { + id: 'id1', + name: 'name1', + label: 'label1', + hints: 'hints1', + isPublished: true, + created: '2016-12-12T10:10', + createdBy: 'Created1', + lastModified: '2017-12-12T10:10', + lastModifiedBy: 'LastModifiedBy1', + fields: [{ + fieldId: 123, + name: 'field1', + isHidden: true, + isDisabled: true, + properties: { + fieldType: 'number' + } + }, { + fieldId: 234, + name: 'field2', + isHidden: true, + isDisabled: true, + properties: { + fieldType: 'string' + } + }, { + fieldId: 345, + name: 'field3', + isHidden: true, + isDisabled: true, + properties: { + fieldType: 'boolean' + } + }] + } + }) + ) + )) + .verifiable(Times.once()); + + let schema: SchemaDetailsDto | null = null; + + schemasService.getSchema('my-app', 'my-schema').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(123, 'field1', true, true, createProperties('number')), + new FieldDto(234, 'field2', true, true, createProperties('string')), + new FieldDto(345, 'field3', true, true, createProperties('boolean')) + ])); + + authService.verifyAll(); + }); + + + 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)) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: { + id: 'my-schema' + } + }) + ) + )) + .verifiable(Times.once()); + + let created: EntityCreatedDto | null = null; + + schemasService.postSchema('my-app', dto).subscribe(result => { + created = result; + }); + + expect(created).toEqual( + new EntityCreatedDto('my-schema')); + + authService.verifyAll(); + }); + + it('should make post request to add field', () => { + const dto = new AddFieldDto('name', createProperties('number')); + + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/fields', dto)) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: { + id: 123 + } + }) + ) + )) + .verifiable(Times.once()); + + let created: EntityCreatedDto | null = null; + + schemasService.postField('my-app', 'my-schema', dto).subscribe(result => { + created = result; + }); + + expect(created).toEqual( + new EntityCreatedDto(123)); + + authService.verifyAll(); + }); + + 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)) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.putSchema('my-app', 'my-schema', dto); + + authService.verifyAll(); + }); + + it('should make put request to update field', () => { + const dto = new UpdateFieldDto(createProperties('number')); + + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', dto)) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.putField('my-app', 'my-schema', 1, dto); + + 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())) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.publishSchema('my-app', 'my-schema'); + + 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())) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.unpublishSchema('my-app', 'my-schema'); + + 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())) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.enableField('my-app', 'my-schema', 1); + + 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())) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.disableField('my-app', 'my-schema', 1); + + 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())) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.showField('my-app', 'my-schema', 1); + + 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())) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.hideField('my-app', 'my-schema', 1); + + 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')) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.deleteField('my-app', 'my-schema', 1); + + authService.verifyAll(); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 5bd65e112..69d59e53a 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -18,7 +18,7 @@ import { import { AuthService } from './auth.service'; -export function createProperties(fieldType: string, values: {} | null = null): FieldPropertiesDto { +export function createProperties(fieldType: string, values: Object | null = null): FieldPropertiesDto { let properties: FieldPropertiesDto; switch (fieldType) { @@ -55,7 +55,7 @@ export class SchemaDto { constructor( public readonly id: string, public readonly name: string, - public readonly label: string, + public readonly label: string | undefined, public readonly isPublished: boolean, public readonly createdBy: string, public readonly lastModifiedBy: string, @@ -71,12 +71,12 @@ export class SchemaDetailsDto { public readonly name: string, public readonly label: string, public readonly hints: string, - public readonly fields: FieldDto[], public readonly isPublished: boolean, public readonly createdBy: string, public readonly lastModifiedBy: string, public readonly created: DateTime, - public readonly lastModified: DateTime + public readonly lastModified: DateTime, + public readonly fields: FieldDto[] ) { } } @@ -103,12 +103,12 @@ export abstract class FieldPropertiesDto { } export class NumberFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string, hints: string, placeholder: string, isRequired: boolean, + constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, isRequired: boolean, public readonly editor: string, - public readonly defaultValue: number | null, - public readonly maxValue: number | null, - public readonly minValue: number | null, - public readonly allowedValues: number[] | undefined + public readonly defaultValue?: number, + public readonly maxValue?: number, + public readonly minValue?: number, + public readonly allowedValues?: number[] ) { super(label, hints, placeholder, isRequired); @@ -117,14 +117,14 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto { } export class StringFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string, hints: string, placeholder: string, isRequired: boolean, + constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, isRequired: boolean, public readonly editor: string, - public readonly defaultValue: string, - public readonly pattern: string, - public readonly patternMessage: string, - public readonly minLength: number | null, - public readonly maxLength: number | null, - public readonly allowedValues: string[] + public readonly defaultValue?: string, + public readonly pattern?: string, + public readonly patternMessage?: string, + public readonly minLength?: number | null, + public readonly maxLength?: number | null, + public readonly allowedValues?: string[] ) { super(label, hints, placeholder, isRequired); @@ -133,9 +133,9 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { } export class BooleanFieldPropertiesDto extends FieldPropertiesDto { - constructor(label: string, hints: string, placeholder: string, isRequired: boolean, + constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, isRequired: boolean, public readonly editor: string, - public readonly defaultValue: boolean | null + public readonly defaultValue?: boolean ) { super(label, hints, placeholder, isRequired); @@ -145,8 +145,8 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto { export class UpdateSchemaDto { constructor( - public readonly label: string, - public readonly hints: string + public readonly label?: string, + public readonly hints?: string ) { } } @@ -229,18 +229,18 @@ export class SchemasService { response.name, response.label, response.hints, - fields, response.isPublished, response.createdBy, response.lastModifiedBy, DateTime.parseISO_UTC(response.created), - DateTime.parseISO_UTC(response.lastModified)); + DateTime.parseISO_UTC(response.lastModified), + fields); }) .catchError('Failed to load schema. Please reload.'); } public postSchema(appName: string, dto: CreateSchemaDto): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); return this.authService.authPost(url, dto) .map(response => response.json()) @@ -251,7 +251,7 @@ export class SchemasService { } public postField(appName: string, schemaName: string, dto: AddFieldDto): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`); return this.authService.authPost(url, dto) .map(response => response.json()) @@ -262,63 +262,63 @@ export class SchemasService { } public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); return this.authService.authPut(url, dto) .catchError('Failed to update schema. Please reload.'); } public publishSchema(appName: string, schemaName: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); return this.authService.authPut(url, {}) .catchError('Failed to publish schema. Please reload.'); } public unpublishSchema(appName: string, schemaName: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`); return this.authService.authPut(url, {}) .catchError('Failed to unpublish schema. Please reload.'); } public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); return this.authService.authPut(url, dto) .catchError('Failed to update field. Please reload.'); } public enableField(appName: string, schemaName: string, fieldId: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`); return this.authService.authPut(url, {}) .catchError('Failed to enable field. Please reload.'); } public disableField(appName: string, schemaName: string, fieldId: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`); return this.authService.authPut(url, {}) .catchError('Failed to disable field. Please reload.'); } public showField(appName: string, schemaName: string, fieldId: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`); return this.authService.authPut(url, {}) .catchError('Failed to show field. Please reload.'); } public hideField(appName: string, schemaName: string, fieldId: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`); return this.authService.authPut(url, {}) .catchError('Failed to hide field. Please reload.'); } public deleteField(appName: string, schemaName: string, fieldId: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); return this.authService.authDelete(url) .catchError('Failed to delete field. Please reload.'); diff --git a/src/Squidex/app/shared/services/users-provider.service.spec.ts b/src/Squidex/app/shared/services/users-provider.service.spec.ts index fb976020c..dd7aa7b59 100644 --- a/src/Squidex/app/shared/services/users-provider.service.spec.ts +++ b/src/Squidex/app/shared/services/users-provider.service.spec.ts @@ -33,7 +33,7 @@ describe('UsersProviderService', () => { usersService.setup(x => x.getUser('123')) .returns(() => Observable.of(user)).verifiable(Times.once()); - let resultingUser: UserDto = null; + let resultingUser: UserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; @@ -52,7 +52,7 @@ describe('UsersProviderService', () => { usersProviderService.getUser('123'); - let resultingUser: UserDto = null; + let resultingUser: UserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; @@ -72,7 +72,7 @@ describe('UsersProviderService', () => { usersService.setup(x => x.getUser('123')) .returns(() => Observable.of(user)).verifiable(Times.once()); - let resultingUser: UserDto = null; + let resultingUser: UserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; @@ -90,7 +90,7 @@ describe('UsersProviderService', () => { usersService.setup(x => x.getUser('123')) .returns(() => Observable.throw('NOT FOUND')).verifiable(Times.once()); - let resultingUser: UserDto = null; + let resultingUser: UserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; diff --git a/src/Squidex/app/shared/services/users.service.spec.ts b/src/Squidex/app/shared/services/users.service.spec.ts index 4bf352d5f..80c93f130 100644 --- a/src/Squidex/app/shared/services/users.service.spec.ts +++ b/src/Squidex/app/shared/services/users.service.spec.ts @@ -46,7 +46,7 @@ describe('UsersService', () => { )) .verifiable(Times.once()); - let user: UserDto[] = null; + let user: UserDto[] | null = null; usersService.getUsers().subscribe(result => { user = result; @@ -82,7 +82,7 @@ describe('UsersService', () => { )) .verifiable(Times.once()); - let user: UserDto[] = null; + let user: UserDto[] | null = null; usersService.getUsers('my-query').subscribe(result => { user = result; @@ -113,7 +113,7 @@ describe('UsersService', () => { )) .verifiable(Times.once()); - let user: UserDto = null; + let user: UserDto | null = null; usersService.getUser('123').subscribe(result => { user = result; diff --git a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs index e2176d05f..521e8e6f0 100644 --- a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs +++ b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs @@ -9,12 +9,23 @@ using System.Collections.Generic; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; using Xunit; namespace Squidex.Core.Contents { public class ContentDataTests { + private readonly Schema schema = + Schema.Create("schema", new SchemaProperties()) + .AddOrUpdateField( + new NumberField(1, "field1", new NumberFieldProperties { IsLocalizable = true })) + .AddOrUpdateField( + new NumberField(2, "field2", new NumberFieldProperties { IsLocalizable = false })); + private readonly Language[] languages = { Language.GetLanguage("de"), Language.GetLanguage("en") }; + private readonly Language masterLanguage = Language.GetLanguage("en"); + [Fact] public void Should_convert_from_dictionary() { @@ -28,12 +39,11 @@ namespace Squidex.Core.Contents }, ["field2"] = new Dictionary { - ["en"] = 1, - ["de"] = 2 + ["iv"] = 3 } }; - var actual = ContentData.Create(input); + var actual = ContentData.FromApiRequest(input); var expected = ContentData.Empty @@ -43,13 +53,114 @@ namespace Squidex.Core.Contents .AddValue("de", "de_string")) .AddField("field2", ContentFieldData.Empty - .AddValue("en", 1) - .AddValue("de", 2)); + .AddValue("iv", 3)); - var output = actual.ToRaw(); + var output = actual.ToApiResponse(schema, languages, masterLanguage); actual.ShouldBeEquivalentTo(expected); output.ShouldBeEquivalentTo(input); } + + [Fact] + public void Should_cleanup_old_fields() + { + var expected = + new Dictionary> + { + ["field1"] = new Dictionary + { + ["en"] = "en_string", + ["de"] = "de_string" + } + }; + + var input = + ContentData.Empty + .AddField("field0", + ContentFieldData.Empty + .AddValue("en", "en_string")) + .AddField("field1", + ContentFieldData.Empty + .AddValue("en", "en_string") + .AddValue("de", "de_string")); + + var output = input.ToApiResponse(schema, languages, masterLanguage); + + output.ShouldBeEquivalentTo(expected); + } + + [Fact] + public void Should_cleanup_old_languages() + { + var expected = + new Dictionary> + { + ["field1"] = new Dictionary + { + ["en"] = "en_string", + ["de"] = "de_string" + } + }; + + var input = + ContentData.Empty + .AddField("field1", + ContentFieldData.Empty + .AddValue("en", "en_string") + .AddValue("de", "de_string") + .AddValue("it", "it_string")); + + var output = input.ToApiResponse(schema, languages, masterLanguage); + + output.ShouldBeEquivalentTo(expected); + } + + [Fact] + public void Should_provide_invariant_from_master_language() + { + var expected = + new Dictionary> + { + ["field2"] = new Dictionary + { + ["iv"] = 3 + } + }; + + var input = + ContentData.Empty + .AddField("field2", + ContentFieldData.Empty + .AddValue("de", 2) + .AddValue("en", 3)); + + var output = input.ToApiResponse(schema, languages, masterLanguage); + + output.ShouldBeEquivalentTo(expected); + } + + [Fact] + public void Should_provide_invariant_from_first_language() + { + var expected = + new Dictionary> + { + ["field2"] = new Dictionary + { + ["iv"] = 2 + } + }; + + var input = + ContentData.Empty + .AddField("field2", + ContentFieldData.Empty + .AddValue("de", 2) + .AddValue("it", 3)); + + var output = input.ToApiResponse(schema, languages, masterLanguage); + + output.ShouldBeEquivalentTo(expected); + } } }