Browse Source

6.8 bugfixes (#897)

* Several bugfixes.

* Fix scheduler and release semaphore.

* Disable delete button if the user does not have permissions.

* Fix concurrency issues with cache grains.

* Map translate code.

* Just some formatting.
pull/904/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
e337e41de7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/i18n/source/backend_en.json
  2. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs
  3. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  4. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs
  5. 31
      backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs
  6. 45
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs
  7. 2
      backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs
  8. 4
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
  9. 2
      backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs
  10. 3
      backend/src/Squidex/Config/Domain/BackupsServices.cs
  11. 22
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs
  12. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs
  13. 46
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs
  14. 48
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs
  15. 25
      backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs
  16. 2
      frontend/src/app/features/content/shared/list/content.component.html
  17. 2
      frontend/src/app/features/content/shared/references/content-creator.component.ts
  18. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html
  19. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html
  20. 4
      frontend/src/app/features/settings/pages/workflows/workflow.component.html
  21. 2
      frontend/src/app/shared/components/references/content-selector.component.ts
  22. 11
      frontend/src/app/shared/state/schema-tag-source.ts

1
backend/i18n/source/backend_en.json

@ -142,6 +142,7 @@
"contents.singletonNotChangeable": "Singleton content cannot be updated.",
"contents.singletonNotCreatable": "Singleton content cannot be created.",
"contents.singletonNotDeletable": "Singleton content cannot be deleted.",
"contents.componentNotCreatable": "Component content cannot be created.",
"contents.statusNotValid": "Status is not defined in the workflow.",
"contents.statusTransitionNotAllowed": "Cannot change status from {oldStatus} to {newStatus}.",
"contents.validation.aspectRatio": "Must have aspect ratio {width}:{height}.",

4
backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs

@ -76,6 +76,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
result.Add(id);
}
else if (appIds.TryGetValue(name, out var cachedId) && cachedId != DomainId.Empty)
{
result.Add(cachedId);
}
else
{
appIds[name] = default;

1
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -229,6 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
private async Task CreateCore(CreateContent c, ContentOperation operation)
{
operation.MustNotCreateComponent();
operation.MustNotCreateSingleton();
operation.MustNotCreateForUnpublishedSchema();
operation.MustHaveData(c.Data);

8
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs

@ -22,6 +22,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
}
}
public static void MustNotCreateComponent(this ContentOperation operation)
{
if (operation.SchemaDef.Type == SchemaType.Component)
{
throw new DomainException(T.Get("contents.componentNotCreatable"));
}
}
public static void MustNotCreateSingleton(this ContentOperation operation)
{
if (operation.SchemaDef.Type == SchemaType.Singleton && operation.CommandId != operation.Schema.Id)

31
backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs

@ -17,7 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
public sealed class RulesCacheGrain : GrainBase, IRulesCacheGrain
{
private readonly IRuleRepository ruleRepository;
private List<DomainId>? ruleIds;
private readonly HashSet<DomainId> ruleIds = new HashSet<DomainId>();
private bool isLoaded;
public RulesCacheGrain(IGrainIdentity grainIdentity, IRuleRepository ruleRepository)
: base(grainIdentity)
@ -25,32 +26,40 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
this.ruleRepository = ruleRepository;
}
public async Task<IReadOnlyCollection<DomainId>> GetRuleIdsAsync()
public override Task OnActivateAsync()
{
var ids = ruleIds;
return GetRuleIdsAsync();
}
if (ids == null)
public async Task<IReadOnlyCollection<DomainId>> GetRuleIdsAsync()
{
if (!isLoaded)
{
ids = await ruleRepository.QueryIdsAsync(Key);
var loaded = await ruleRepository.QueryIdsAsync(Key);
foreach (var id in loaded)
{
ruleIds.Add(id);
}
ruleIds = ids;
isLoaded = true;
}
return ids;
return ruleIds;
}
public Task AddAsync(DomainId id)
{
ruleIds?.Add(id);
ruleIds.Add(id);
return Task.CompletedTask;
}
public Task RemoveAsync(DomainId id)
public async Task RemoveAsync(DomainId id)
{
ruleIds?.Remove(id);
await GetRuleIdsAsync();
return Task.CompletedTask;
ruleIds.Remove(id);
}
}
}

45
backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs

@ -17,7 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
public sealed class SchemasCacheGrain : UniqueNameGrain<DomainId>, ISchemasCacheGrain
{
private readonly ISchemaRepository schemaRepository;
private Dictionary<string, DomainId>? schemaIds;
private readonly Dictionary<string, DomainId> schemaIds = new Dictionary<string, DomainId>();
private bool isLoaded;
public SchemasCacheGrain(IGrainIdentity identity, ISchemaRepository schemaRepository)
: base(identity)
@ -25,6 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
this.schemaRepository = schemaRepository;
}
public override Task OnActivateAsync()
{
return GetIdsAsync();
}
public override async Task<string?> ReserveAsync(DomainId id, string name)
{
var token = await base.ReserveAsync(id, name);
@ -61,41 +67,38 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
private async Task<Dictionary<string, DomainId>> GetIdsAsync()
{
var ids = schemaIds;
if (ids == null)
if (!isLoaded)
{
ids = await schemaRepository.QueryIdsAsync(Key);
var loaded = await schemaRepository.QueryIdsAsync(Key);
schemaIds = ids;
foreach (var (name, id) in loaded)
{
schemaIds[name] = id;
}
isLoaded = true;
}
return ids;
return schemaIds;
}
public Task AddAsync(DomainId id, string name)
{
if (schemaIds != null)
{
schemaIds[name] = id;
}
schemaIds[name] = id;
return Task.CompletedTask;
}
public Task RemoveAsync(DomainId id)
public async Task RemoveAsync(DomainId id)
{
if (schemaIds != null)
{
var name = schemaIds.FirstOrDefault(x => x.Value == id).Key;
await GetIdsAsync();
if (name != null)
{
schemaIds.Remove(name);
}
}
var name = schemaIds.FirstOrDefault(x => x.Value == id).Key;
return Task.CompletedTask;
if (name != null)
{
schemaIds.Remove(name);
}
}
}
}

2
backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs

@ -113,6 +113,8 @@ namespace Squidex.Infrastructure.Tasks
}
finally
{
semaphore.Release();
if (Interlocked.Decrement(ref pendingTasks) <= 1)
{
tcs.TrySetResult(true);

4
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs

@ -154,12 +154,12 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchema), values));
if (resources.CanReadContent(Name))
if (resources.CanReadContent(Name) && Type == SchemaType.Default)
{
AddGetLink("contents", resources.Url<ContentsController>(x => nameof(x.GetContents), values));
}
if (resources.CanCreateContent(Name))
if (resources.CanCreateContent(Name) && Type == SchemaType.Default)
{
AddPostLink("contents/create", resources.Url<ContentsController>(x => nameof(x.PostContent), values));
AddPostLink("contents/create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true");

2
backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs

@ -24,7 +24,7 @@ namespace Squidex.Areas.Api.Controllers.Translations.Models
public static TranslationDto FromDomain(TranslationResult translation)
{
return SimpleMapper.Map(translation, new TranslationDto());
return SimpleMapper.Map(translation, new TranslationDto { Result = translation.Code });
}
}
}

3
backend/src/Squidex/Config/Domain/BackupsServices.cs

@ -22,6 +22,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<TempFolderBackupArchiveLocation>()
.As<IBackupArchiveLocation>();
services.AddSingletonAs<DefaultBackupHandlerFactory>()
.As<IBackupHandlerFactory>();
services.AddSingletonAs<DefaultBackupArchiveStore>()
.As<IBackupArchiveStore>();

22
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs

@ -149,5 +149,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
A.CallTo(() => appRepository.QueryIdsAsync(A<IEnumerable<string>>.That.Is("name1", "name2"), default))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_merge_found_value_with_added_id()
{
var foundId = DomainId.NewGuid();
async Task<Dictionary<string, DomainId>> GetIds()
{
await sut.AddAsync(foundId, "name1");
return new Dictionary<string, DomainId>();
}
A.CallTo(() => appRepository.QueryIdsAsync(A<IEnumerable<string>>._, A<CancellationToken>._))
.ReturnsLazily(() => GetIds());
var result1 = await sut.GetAppIdsAsync(new[] { "name1" });
var result2 = await sut.GetAppIdsAsync(new[] { "name1" });
Assert.Equal(foundId, result1.Single());
Assert.Equal(foundId, result2.Single());
}
}
}

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs

@ -34,6 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
private readonly ISchemaEntity normalUnpublishedSchema;
private readonly ISchemaEntity singletonSchema;
private readonly ISchemaEntity singletonUnpublishedSchema;
private readonly ISchemaEntity componentSchema;
private readonly RefToken actor = RefToken.User("123");
public GuardContentTests()
@ -49,6 +50,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
singletonSchema =
Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton).Publish());
componentSchema =
Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Component).Publish());
}
[Fact]
@ -91,6 +95,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
Assert.Throws<DomainException>(() => operation.MustNotCreateSingleton());
}
[Fact]
public void Should_throw_exception_if_creating_component_content()
{
var operation = Operation(CreateContent(Status.Draft), componentSchema);
Assert.Throws<DomainException>(() => operation.MustNotCreateComponent());
}
[Fact]
public void Should_not_throw_exception_if_creating_singleton_content_with_schema_id()
{

46
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs

@ -99,5 +99,51 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_remove_id_from_not_loaded_result()
{
var ids = new List<DomainId>
{
DomainId.NewGuid(),
DomainId.NewGuid()
};
var newId = DomainId.NewGuid();
A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default))
.Returns(ids);
await sut.RemoveAsync(ids.ElementAt(0));
var result = await sut.GetRuleIdsAsync();
Assert.Equal(ids.Skip(1), result);
A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_merge_found_value_with_added_id()
{
var foundId = DomainId.NewGuid();
async Task<List<DomainId>> GetIds()
{
await sut.AddAsync(foundId);
return new List<DomainId>();
}
A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default))
.ReturnsLazily(() => GetIds());
var result1 = await sut.GetRuleIdsAsync();
var result2 = await sut.GetRuleIdsAsync();
Assert.Equal(foundId, result1.Single());
Assert.Equal(foundId, result2.Single());
}
}
}

48
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs

@ -112,10 +112,56 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
var result = await sut.GetSchemaIdsAsync();
Assert.Equal(ids.Values.Take(1), result);
Assert.Equal(ids.Values.Skip(1), result);
A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_remove_id_from_not_loaded_result()
{
var ids = new Dictionary<string, DomainId>
{
["name1"] = DomainId.NewGuid(),
["name2"] = DomainId.NewGuid()
};
var newId = DomainId.NewGuid();
A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default))
.Returns(ids);
await sut.RemoveAsync(ids.ElementAt(0).Value);
var result = await sut.GetSchemaIdsAsync();
Assert.Equal(ids.Values.Skip(1), result);
A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_merge_found_value_with_added_id()
{
var foundId = DomainId.NewGuid();
async Task<Dictionary<string, DomainId>> GetIds()
{
await sut.AddAsync(foundId, "name1");
return new Dictionary<string, DomainId>();
}
A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default))
.ReturnsLazily(() => GetIds());
var result1 = await sut.GetSchemaIdsAsync();
var result2 = await sut.GetSchemaIdsAsync();
Assert.Equal(foundId, result1.Single());
Assert.Equal(foundId, result2.Single());
}
}
}

25
backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs

@ -25,6 +25,21 @@ namespace Squidex.Infrastructure.Tasks
Assert.Equal(new[] { 1 }, results.ToArray());
}
[Fact]
public async Task Should_schedule_lot_of_tasks_with_limited_concurrency()
{
var limited = new Scheduler(1);
for (var i = 1; i <= 10; i++)
{
Schedule(i, limited);
}
await limited.CompleteAsync();
Assert.Equal(Enumerable.Range(1, 10).ToArray(), results.OrderBy(x => x).ToArray());
}
[Fact]
public async Task Should_schedule_multiple_tasks()
{
@ -83,5 +98,15 @@ namespace Squidex.Infrastructure.Tasks
results.Add(value);
});
}
private void Schedule(int value, Scheduler target)
{
target.Schedule(async _ =>
{
await Task.Delay(1);
results.Add(value);
});
}
}
}

2
frontend/src/app/features/content/shared/list/content.component.html

@ -43,7 +43,7 @@
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete"
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"

2
frontend/src/app/features/content/shared/references/content-creator.component.ts

@ -56,7 +56,7 @@ export class ContentCreatorComponent extends ResourceOwner implements OnInit {
this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canContentsCreate);
if (this.schemaIds && this.schemaIds.length > 0) {
this.schemas = this.schemas.filter(x => this.schemaIds!.indexOf(x.id) >= 0);
this.schemas = this.schemas.filter(x => x.type === 'Default' && this.schemaIds!.indexOf(x.id) >= 0);
}
const selectedSchema = this.schemas.find(x => x.name === this.schemaName) || this.schemas[0];

2
frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html

@ -4,7 +4,7 @@
<div class="col-9">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds"
[converter]="(schemasSource.converter | async)!" [suggestions]="(schemasSource.converter | async)?.suggestions">
[converter]="(schemasSource.normalConverter | async)!" [suggestions]="(schemasSource.normalConverter | async)?.suggestions">
</sqx-tag-editor>
</div>
</div>

2
frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html

@ -91,7 +91,7 @@
<div class="col-9">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds"
[converter]="(schemasSource.converter | async)!" [suggestions]="(schemasSource.converter | async)?.suggestions">
[converter]="(schemasSource.normalConverter | async)!" [suggestions]="(schemasSource.normalConverter | async)?.suggestions">
</sqx-tag-editor>
</div>
</div>

4
frontend/src/app/features/settings/pages/workflows/workflow.component.html

@ -4,7 +4,7 @@
<span class="workflow-name">{{workflow.displayName}}</span>
</div>
<div class="col col-tags">
<sqx-tag-editor [converter]="(schemasSource.converter | async)!" [ngModel]="workflow.schemaIds"
<sqx-tag-editor [converter]="(schemasSource.normalConverter | async)!" [ngModel]="workflow.schemaIds"
[styleBlank]="true"
[singleLine]="true"
[readonly]="true">
@ -81,7 +81,7 @@
[disabled]="!isEditable"
[ngModel]="workflow.schemaIds"
(ngModelChange)="changeSchemaIds($event)"
[suggestions]="(schemasSource.converter | async)?.suggestions">
[suggestions]="(schemasSource.normalConverter | async)?.suggestions">
</sqx-tag-editor>
<sqx-form-hint>

2
frontend/src/app/shared/components/references/content-selector.component.ts

@ -81,7 +81,7 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit {
this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canReadContents);
if (this.schemaIds && this.schemaIds.length > 0) {
this.schemas = this.schemas.filter(x => x.canReadContents && this.schemaIds!.includes(x.id));
this.schemas = this.schemas.filter(x => x.type === 'Default' && x.canReadContents && this.schemaIds!.includes(x.id));
}
this.selectSchema(this.schemas[0]);

11
frontend/src/app/shared/state/schema-tag-source.ts

@ -16,7 +16,12 @@ class SchemaConverter implements TagConverter {
constructor(
private readonly schemas: ReadonlyArray<SchemaDto>,
normalOnly: boolean,
) {
if (normalOnly) {
schemas = schemas.filter(x => x.type === 'Default');
}
this.suggestions = schemas.map(x => new TagValue(x.id, x.name, x.id));
}
@ -45,7 +50,11 @@ class SchemaConverter implements TagConverter {
export class SchemaTagSource {
public converter =
this.schemasState.schemas.pipe(
map(x => new SchemaConverter(x)));
map(x => new SchemaConverter(x, false)));
public normalConverter =
this.schemasState.schemas.pipe(
map(x => new SchemaConverter(x, true)));
constructor(
readonly schemasState: SchemasState,

Loading…
Cancel
Save