Browse Source

Optimisic concurrency

pull/1/head
Sebastian 9 years ago
parent
commit
ee3857bd81
  1. 9
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs
  2. 3
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  3. 2
      src/Squidex.Infrastructure/CQRS/DomainObject.cs
  4. 10
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs
  5. 12
      src/Squidex.Write/Apps/AppCommandHandler.cs
  6. 11
      src/Squidex.Write/Contents/ContentCommandHandler.cs
  7. 6
      src/Squidex/Config/Domain/WriteModule.cs
  8. 2
      src/Squidex/Controllers/Api/Languages/LanguagesController.cs
  9. 5
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs
  10. 5
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs
  11. 11
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  12. 5
      src/Squidex/Controllers/ContentApi/Models/ContentDto.cs
  13. 2
      src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs
  14. 6
      src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs
  15. 8
      src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs
  16. 2
      src/Squidex/Views/Shared/Docs.cshtml
  17. 12
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  18. 31
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  19. 6
      src/Squidex/app/features/content/pages/messages.ts
  20. 3
      src/Squidex/app/features/schemas/pages/messages.ts
  21. 8
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  22. 1
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  23. 27
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  24. 16
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  25. 6
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  26. 2
      src/Squidex/app/features/settings/pages/clients/client.component.html
  27. 13
      src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
  28. 13
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  29. 17
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  30. 7
      src/Squidex/app/framework/angular/http-utils.ts
  31. 1
      src/Squidex/app/framework/declarations.ts
  32. 21
      src/Squidex/app/framework/utils/version.ts
  33. 6
      src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts
  34. 2
      src/Squidex/app/shared/guards/resolve-app-languages.guard.ts
  35. 6
      src/Squidex/app/shared/guards/resolve-content.guard.spec.ts
  36. 2
      src/Squidex/app/shared/guards/resolve-content.guard.ts
  37. 8
      src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts
  38. 2
      src/Squidex/app/shared/guards/resolve-published-schema.guard.ts
  39. 6
      src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts
  40. 2
      src/Squidex/app/shared/guards/resolve-schema.guard.ts
  41. 20
      src/Squidex/app/shared/services/app-clients.service.spec.ts
  42. 18
      src/Squidex/app/shared/services/app-clients.service.ts
  43. 16
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  44. 14
      src/Squidex/app/shared/services/app-contributors.service.ts
  45. 20
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  46. 18
      src/Squidex/app/shared/services/app-languages.service.ts
  47. 49
      src/Squidex/app/shared/services/auth.service.ts
  48. 49
      src/Squidex/app/shared/services/contents.service.spec.ts
  49. 36
      src/Squidex/app/shared/services/contents.service.ts
  50. 91
      src/Squidex/app/shared/services/schemas.service.spec.ts
  51. 57
      src/Squidex/app/shared/services/schemas.service.ts
  52. 4
      src/Squidex/app/theme/_bootstrap.scss

9
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;
}
}
}

3
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();

2
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;

10
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<MongoContentEntity> 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<IReadOnlyList<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> 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)
{

12
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<AppDomainObject>(context, x =>
{
context.Succeed(command.AggregateId);
});
await handler.CreateAsync<AppDomainObject>(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<AppDomainObject>(context, x =>
{
context.Succeed(new EntitySavedResult(x.Version));
});
await handler.UpdateAsync<AppDomainObject>(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<AppClient>(x.Clients[command.Id], x.Version));
});
}

11
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);

6
src/Squidex/Config/Domain/WriteModule.cs

@ -72,11 +72,11 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, 0)))
builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, -1)))
.AsSelf()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<ContentDomainObject>>(c => (id => new ContentDomainObject(id, 0)))
builder.Register<DomainObjectFactoryFunction<ContentDomainObject>>(c => (id => new ContentDomainObject(id, -1)))
.AsSelf()
.SingleInstance();
@ -84,7 +84,7 @@ namespace Squidex.Config.Domain
{
var fieldRegistry = c.Resolve<FieldRegistry>();
return (id => new SchemaDomainObject(id, 0, fieldRegistry));
return (id => new SchemaDomainObject(id, -1, fieldRegistry));
})
.AsSelf()
.SingleInstance();

2
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);
}

5
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.
/// </summary>
public Instant LastModified { get; set; }
/// <summary>
/// The version of the schema.
/// </summary>
public int Version { get; set; }
}
}

5
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.
/// </summary>
public Instant LastModified { get; set; }
/// <summary>
/// The version of the schema.
/// </summary>
public int Version { get; set; }
}
}

11
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<IActionResult> 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<IActionResult> 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)
{

5
src/Squidex/Controllers/ContentApi/Models/ContentDto.cs

@ -52,5 +52,10 @@ namespace Squidex.Controllers.ContentApi.Models
/// Indicates if the content element is publihed.
/// </summary>
public bool IsPublished { get; set; }
/// <summary>
/// The version of the content.
/// </summary>
public int Version { get; set; }
}
}

2
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)

6
src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs

@ -26,10 +26,10 @@ namespace Squidex.Pipeline.CommandHandlers
public Task<bool> 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;
}

8
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)
{

2
src/Squidex/Views/Shared/Docs.cshtml

@ -28,6 +28,6 @@
<body>
<redoc lazy-rendering="true" spec-url="@Url.Content(ViewBag.Specification)"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"></script>
<script src="https://rebilly.github.io/ReDoc/releases/v1.10.1/redoc.min.js"></script>
</body>
</html>

12
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 => {

31
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;
}

6
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
) {
}
}

3
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
) {
}
}

8
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));

1
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -82,6 +82,7 @@
<sqx-schema-edit-form
[appName]="appName() | async"
[schema]="schemaProperties"
[version]="version"
(saved)="onSchemaSaved($event)"
(cancelled)="editSchemaDialog.hide()"></sqx-schema-edit-form>
</div>

27
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<FieldDto>();
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));
}
}

16
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);
}
}

6
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));
}

2
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -22,7 +22,7 @@
<button type="submit" class="btn btn-primary" [disabled]="!renameForm.valid">Save</button>
<button class="btn btn-default btn-cancel" (click)="cancelRename()">
<button class="btn btn-simple btn-cancel" (click)="cancelRename()">
<i class="icon-close"></i>
</button>
</form>

13
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<AppClientDto>;
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();

13
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<AppContributorDto>();
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 => {

17
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<AppLanguageDto>();
@ -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<AppLanguageDto>) {

7
src/Squidex/app/framework/angular/http-utils.ts

@ -53,12 +53,17 @@ export function catchError(message: string): Observable<any> {
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();
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);
});
}

1
src/Squidex/app/framework/declarations.ts

@ -50,3 +50,4 @@ export * from './utils/immutable-array';
export * from './utils/math-helper';
export * from './utils/modal-view';
export * from './utils/string-helper';
export * from './utils/version';

21
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;
}
}

6
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();

2
src/Squidex/app/shared/guards/resolve-app-languages.guard.ts

@ -26,7 +26,7 @@ export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> {
}
const result =
this.appLanguagesService.getLanguages(appName).toPromise()
this.appLanguagesService.getLanguages(appName, null).toPromise()
.then(dto => {
if (!dto) {
this.router.navigate(['/404']);

6
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();

2
src/Squidex/app/shared/guards/resolve-content.guard.ts

@ -28,7 +28,7 @@ export class ResolveContentGuard implements Resolve<ContentDto> {
}
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']);

8
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();

2
src/Squidex/app/shared/guards/resolve-published-schema.guard.ts

@ -27,7 +27,7 @@ export class ResolvePublishedSchemaGuard implements Resolve<SchemaDetailsDto> {
}
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']);

6
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();

2
src/Squidex/app/shared/guards/resolve-schema.guard.ts

@ -27,7 +27,7 @@ export class ResolveSchemaGuard implements Resolve<SchemaDetailsDto> {
}
const result =
this.schemasService.getSchema(appName, schemaName).toPromise()
this.schemasService.getSchema(appName, schemaName, null).toPromise()
.then(dto => {
if (!dto) {
this.router.navigate(['/404']);

20
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<AuthService>;
let appClientsService: AppClientsService;
let version = new Version('1');
let http: IMock<Http>;
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();
});

18
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<AppClientDto[]> {
public getClients(appName: string, version: Version): Observable<AppClientDto[]> {
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<AppClientDto> {
public postClient(appName: string, dto: CreateAppClientDto, version: Version): Observable<AppClientDto> {
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<any> {
public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version: Version): Observable<any> {
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<any> {
public deleteClient(appName: string, id: string, version: Version): Observable<any> {
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.');
}

16
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<AuthService>;
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();
});

14
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<AppContributorDto[]> {
public getContributors(appName: string, version: Version): Observable<AppContributorDto[]> {
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<any> {
public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<any> {
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<any> {
public deleteContributor(appName: string, contributorId: string, version: Version): Observable<any> {
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.');
}
}

20
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<AuthService>;
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();
});

18
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<AppLanguageDto[]> {
public getLanguages(appName: string, version: Version): Observable<AppLanguageDto[]> {
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<AppLanguageDto> {
public postLanguages(appName: string, dto: AddAppLanguageDto, version: Version): Observable<AppLanguageDto> {
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<any> {
public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable<any> {
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<any> {
public deleteLanguage(appName: string, languageCode: string, version: Version): Observable<any> {
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.');
}
}

49
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,32 +169,42 @@ export class AuthService {
return resultPromise;
}
public authGet(url: string, options?: RequestOptions): Observable<Response> {
options = this.setRequestOptions(options);
public authGet(url: string, version?: Version, options?: RequestOptions): Observable<Response> {
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<Response> {
options = this.setRequestOptions(options);
public authPut(url: string, data: any, version?: Version, options?: RequestOptions): Observable<Response> {
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<Response> {
options = this.setRequestOptions(options);
public authDelete(url: string, version?: Version, options?: RequestOptions): Observable<Response> {
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<Response> {
options = this.setRequestOptions(options);
public authPost(url: string, data: any, version?: Version, options?: RequestOptions): Observable<Response> {
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<Response>) {
return response.catch((error: Response) => {
private checkResponse(responseStream: Observable<Response>, version?: Version) {
return responseStream
.do((response: Response) => {
if (version && response.status.toString().indexOf('2') === 0) {
const etag = response.headers.get('etag');
if (etag) {
version.update(etag);
}
}
})
.catch((error: Response) => {
if (error.status === 401 || error.status === 404) {
this.logoutRedirect();
@ -208,7 +218,7 @@ export class AuthService {
});
}
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;
}
}

49
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<AuthService>;
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();
});

36
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<ContentDto> {
public getContent(appName: string, schemaName: string, id: string, version: Version): Observable<ContentDto> {
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<EntityCreatedDto> {
public postContent(appName: string, schemaName: string, dto: any, version: Version): Observable<EntityCreatedDto> {
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<any> {
public putContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable<any> {
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<any> {
public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> {
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<any> {
public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> {
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<any> {
public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> {
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.');
}
}

91
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<AuthService>;
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();
});

57
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<SchemaDetailsDto> {
public getSchema(appName: string, id: string, version: Version): Observable<SchemaDetailsDto> {
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<EntityCreatedDto> {
public postSchema(appName: string, dto: CreateSchemaDto, version: Version): Observable<EntityCreatedDto> {
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<EntityCreatedDto> {
public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable<EntityCreatedDto> {
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<any> {
public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable<any> {
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<any> {
public publishSchema(appName: string, schemaName: string, version: Version): Observable<any> {
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<any> {
public unpublishSchema(appName: string, schemaName: string, version: Version): Observable<any> {
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<any> {
public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable<any> {
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<any> {
public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
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<any> {
public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
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<any> {
public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
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<any> {
public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
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<any> {
public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
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.');
}
}

4
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;
}

Loading…
Cancel
Save