mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
41 changed files with 1446 additions and 17 deletions
@ -0,0 +1,62 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using MongoDB.Bson; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
|
|||
public static class IndexParser |
|||
{ |
|||
public static bool TryParse(BsonDocument source, string prefix, [MaybeNullWhen(false)] out IndexDefinition index) |
|||
{ |
|||
index = null!; |
|||
|
|||
if (!source.TryGetValue("name", out var name) || name.BsonType != BsonType.String) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (!name.AsString.StartsWith(prefix, StringComparison.Ordinal)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (!source.TryGetValue("key", out var keys) || keys.BsonType != BsonType.Document) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var definition = new IndexDefinition(); |
|||
foreach (var property in keys.AsBsonDocument) |
|||
{ |
|||
if (property.Value.BsonType != BsonType.Int32) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var fieldName = Adapt.MapPathReverse(property.Name).ToString(); |
|||
|
|||
var order = property.Value.AsInt32 < 0 ? |
|||
SortOrder.Descending : |
|||
SortOrder.Ascending; |
|||
|
|||
definition.Add(new IndexField(fieldName, order)); |
|||
} |
|||
|
|||
if (definition.Count == 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
index = definition; |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Jobs; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Indexes; |
|||
|
|||
public sealed class CreateIndexJob : IJobRunner |
|||
{ |
|||
public const string TaskName = "createIndex"; |
|||
public const string ArgAppId = "appId"; |
|||
public const string ArgAppName = "appName"; |
|||
public const string ArgSchemaId = "schemaId"; |
|||
public const string ArgSchemaName = "schemaName"; |
|||
public const string ArgFieldName = "field_"; |
|||
private readonly IContentRepository contentRepository; |
|||
|
|||
public string Name => TaskName; |
|||
|
|||
public CreateIndexJob(IContentRepository contentRepository) |
|||
{ |
|||
this.contentRepository = contentRepository; |
|||
} |
|||
|
|||
public static JobRequest BuildRequest(RefToken actor, App app, Schema schema, IndexDefinition index) |
|||
{ |
|||
Guard.NotNull(actor); |
|||
Guard.NotNull(app); |
|||
Guard.NotNull(schema); |
|||
Guard.NotNull(index); |
|||
|
|||
var args = new Dictionary<string, string> |
|||
{ |
|||
[ArgAppId] = app.Id.ToString(), |
|||
[ArgAppName] = app.Name, |
|||
[ArgSchemaId] = schema.Id.ToString(), |
|||
[ArgSchemaName] = schema.Name |
|||
}; |
|||
|
|||
foreach (var field in index) |
|||
{ |
|||
args[$"{ArgFieldName}{field.Name}"] = field.Order.ToString(); |
|||
} |
|||
|
|||
return JobRequest.Create( |
|||
actor, |
|||
TaskName, |
|||
args) with |
|||
{ |
|||
AppId = app.NamedId() |
|||
}; |
|||
} |
|||
|
|||
public async Task RunAsync(JobRunContext context, |
|||
CancellationToken ct) |
|||
{ |
|||
// The other arguments are just there for debugging purposes. Therefore do not validate them.
|
|||
if (!context.Job.Arguments.TryGetValue(ArgSchemaId, out var schemaId)) |
|||
{ |
|||
throw new DomainException($"Argument '{ArgSchemaId}' missing."); |
|||
} |
|||
|
|||
if (!context.Job.Arguments.TryGetValue(ArgSchemaName, out var schemaName)) |
|||
{ |
|||
throw new DomainException($"Argument '{ArgSchemaName}' missing."); |
|||
} |
|||
|
|||
var index = new IndexDefinition(); |
|||
|
|||
foreach (var (arg, value) in context.Job.Arguments) |
|||
{ |
|||
if (!arg.StartsWith(ArgFieldName, StringComparison.Ordinal)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var field = arg[ArgFieldName.Length..]; |
|||
|
|||
if (!Enum.TryParse<SortOrder>(value, out var order)) |
|||
{ |
|||
throw new DomainException($"Invalid sort order {order} for field {field}."); |
|||
} |
|||
|
|||
index.Add(new IndexField(field, order)); |
|||
} |
|||
|
|||
if (index.Count == 0) |
|||
{ |
|||
throw new DomainException("Index does not contain an field."); |
|||
} |
|||
|
|||
// Use a readable name to describe the job.
|
|||
context.Job.Description = $"Schema {schemaName}: Create index {index.ToName()}"; |
|||
|
|||
await contentRepository.CreateIndexAsync(context.OwnerId, DomainId.Create(schemaId), index, ct); |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Jobs; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Indexes; |
|||
|
|||
public sealed class DropIndexJob : IJobRunner |
|||
{ |
|||
public const string TaskName = "dropIndex"; |
|||
public const string ArgAppId = "appId"; |
|||
public const string ArgAppName = "appName"; |
|||
public const string ArgSchemaId = "schemaId"; |
|||
public const string ArgSchemaName = "schemaName"; |
|||
public const string ArgIndexName = "indexName"; |
|||
private readonly IContentRepository contentRepository; |
|||
|
|||
public string Name => TaskName; |
|||
|
|||
public DropIndexJob(IContentRepository contentRepository) |
|||
{ |
|||
this.contentRepository = contentRepository; |
|||
} |
|||
|
|||
public static JobRequest BuildRequest(RefToken actor, App app, Schema schema, string name) |
|||
{ |
|||
Guard.NotNull(actor); |
|||
Guard.NotNull(app); |
|||
Guard.NotNull(schema); |
|||
Guard.NotNullOrEmpty(name); |
|||
|
|||
return JobRequest.Create( |
|||
actor, |
|||
TaskName, |
|||
new Dictionary<string, string> |
|||
{ |
|||
[ArgAppId] = app.Id.ToString(), |
|||
[ArgAppName] = app.Name, |
|||
[ArgSchemaId] = schema.Id.ToString(), |
|||
[ArgSchemaName] = schema.Name, |
|||
[ArgIndexName] = name |
|||
}) with |
|||
{ |
|||
AppId = app.NamedId() |
|||
}; |
|||
} |
|||
|
|||
public async Task RunAsync(JobRunContext context, |
|||
CancellationToken ct) |
|||
{ |
|||
// The other arguments are just there for debugging purposes. Therefore do not validate them.
|
|||
if (!context.Job.Arguments.TryGetValue(ArgSchemaId, out var schemaId)) |
|||
{ |
|||
throw new DomainException($"Argument '{ArgSchemaId}' missing."); |
|||
} |
|||
|
|||
if (!context.Job.Arguments.TryGetValue(ArgSchemaName, out var schemaName)) |
|||
{ |
|||
throw new DomainException($"Argument '{ArgSchemaName}' missing."); |
|||
} |
|||
|
|||
if (!context.Job.Arguments.TryGetValue(ArgIndexName, out var indexName)) |
|||
{ |
|||
throw new DomainException($"Argument '{ArgIndexName}' missing."); |
|||
} |
|||
|
|||
// Use a readable name to describe the job.
|
|||
context.Job.Description = $"Schema {schemaName}: Drop index {indexName}"; |
|||
|
|||
await contentRepository.DropIndexAsync(context.OwnerId, DomainId.Create(schemaId), indexName, ct); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using Squidex.Infrastructure.Queries; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Infrastructure.States; |
|||
|
|||
public sealed class IndexDefinition : List<IndexField> |
|||
{ |
|||
public string ToName() |
|||
{ |
|||
var sb = new StringBuilder(); |
|||
|
|||
foreach (var field in this) |
|||
{ |
|||
if (sb.Length > 0) |
|||
{ |
|||
sb.Append('_'); |
|||
} |
|||
|
|||
sb.Append(field.Name); |
|||
sb.Append('_'); |
|||
|
|||
if (field.Order == SortOrder.Ascending) |
|||
{ |
|||
sb.Append("asc"); |
|||
} |
|||
else |
|||
{ |
|||
sb.Append("desc"); |
|||
} |
|||
} |
|||
|
|||
return sb.ToString(); |
|||
} |
|||
} |
|||
|
|||
public sealed record IndexField(string Name, SortOrder Order); |
|||
@ -0,0 +1,34 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.States; |
|||
using Squidex.Infrastructure.Validation; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Schemas.Models; |
|||
|
|||
[OpenApiRequest] |
|||
public sealed class CreateIndexDto |
|||
{ |
|||
/// <summary>
|
|||
/// The index fields.
|
|||
/// </summary>
|
|||
[LocalizedRequired] |
|||
public List<IndexFieldDto> Fields { get; set; } |
|||
|
|||
public IndexDefinition ToIndex() |
|||
{ |
|||
var result = new IndexDefinition(); |
|||
|
|||
foreach (var field in Fields) |
|||
{ |
|||
result.Add(new IndexField(field.Name, field.Order)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.States; |
|||
using Squidex.Infrastructure.Validation; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Schemas.Models; |
|||
|
|||
public sealed class IndexDto : Resource |
|||
{ |
|||
/// <summary>
|
|||
/// The name of the index.
|
|||
/// </summary>
|
|||
[LocalizedRequired] |
|||
public string Name { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The index fields.
|
|||
/// </summary>
|
|||
[LocalizedRequired] |
|||
public List<IndexFieldDto> Fields { get; set; } |
|||
|
|||
public static IndexDto FromDomain(IndexDefinition index, Resources resources) |
|||
{ |
|||
var result = new IndexDto |
|||
{ |
|||
Name = index.ToName(), |
|||
Fields = index.Select(IndexFieldDto.FromDomain).ToList(), |
|||
}; |
|||
|
|||
return result.CreateLinks(resources); |
|||
} |
|||
|
|||
private IndexDto CreateLinks(Resources resources) |
|||
{ |
|||
var values = new { app = resources.App, schema = resources.Schema, name = Name }; |
|||
|
|||
if (resources.CanManageIndexes(resources.Schema!)) |
|||
{ |
|||
AddDeleteLink("delete", |
|||
resources.Url<SchemaIndexesController>(x => nameof(x.DeleteIndex), values)); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Queries; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.States; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Schemas.Models; |
|||
|
|||
public sealed class IndexFieldDto |
|||
{ |
|||
/// <summary>
|
|||
/// The name of the field.
|
|||
/// </summary>
|
|||
[LocalizedRequired] |
|||
public string Name { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The sort order of the field.
|
|||
/// </summary>
|
|||
public SortOrder Order { get; set; } |
|||
|
|||
public static IndexFieldDto FromDomain(IndexField field) |
|||
{ |
|||
return SimpleMapper.Map(field, new IndexFieldDto()); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.States; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Schemas.Models; |
|||
|
|||
public sealed class IndexesDto : Resource |
|||
{ |
|||
/// <summary>
|
|||
/// The indexes.
|
|||
/// </summary>
|
|||
public IndexDto[] Items { get; set; } |
|||
|
|||
public static IndexesDto FromDomain(List<IndexDefinition> indexes, Resources resources) |
|||
{ |
|||
var result = new IndexesDto |
|||
{ |
|||
Items = indexes.Select(x => IndexDto.FromDomain(x, resources)).ToArray() |
|||
}; |
|||
|
|||
return result.CreateLinks(resources); |
|||
} |
|||
|
|||
private IndexesDto CreateLinks(Resources resources) |
|||
{ |
|||
var values = new { app = resources.App, schema = resources.Schema }; |
|||
|
|||
AddSelfLink(resources.Url<SchemaIndexesController>(x => nameof(x.GetIndexes), values)); |
|||
|
|||
if (resources.CanManageIndexes(resources.Schema!)) |
|||
{ |
|||
AddPostLink("create", |
|||
resources.Url<SchemaIndexesController>(x => nameof(x.PostIndex), values)); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Squidex.Areas.Api.Controllers.Schemas.Models; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Contents.Indexes; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Jobs; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Security; |
|||
using Squidex.Shared; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Schemas; |
|||
|
|||
/// <summary>
|
|||
/// Update and query information about schemas.
|
|||
/// </summary>
|
|||
[ApiExplorerSettings(GroupName = nameof(Schemas))] |
|||
[ApiModelValidation(true)] |
|||
public class SchemaIndexesController : ApiController |
|||
{ |
|||
private readonly ICommandBus commandBus; |
|||
private readonly IJobService jobService; |
|||
private readonly IContentRepository contentRepository; |
|||
|
|||
public SchemaIndexesController(ICommandBus commandBus, IJobService jobService, IContentRepository contentRepository) |
|||
: base(commandBus) |
|||
{ |
|||
this.commandBus = commandBus; |
|||
this.jobService = jobService; |
|||
this.contentRepository = contentRepository; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the schema indexes.
|
|||
/// </summary>
|
|||
/// <param name="app">The name of the app.</param>
|
|||
/// <param name="schema">The name of the schema.</param>
|
|||
/// <response code="200">Schema indexes returned.</response>
|
|||
/// <response code="404">Schema or app not found.</response>
|
|||
[HttpGet] |
|||
[Route("apps/{app}/schemas/{schema}/indexes/")] |
|||
[ProducesResponseType(typeof(IndexesDto), StatusCodes.Status200OK)] |
|||
[ApiPermissionOrAnonymous(PermissionIds.AppSchemasIndexes)] |
|||
[ApiCosts(1)] |
|||
public async Task<IActionResult> GetIndexes(string app, string schema) |
|||
{ |
|||
var indexes = await contentRepository.GetIndexesAsync(App.Id, Schema.Id, HttpContext.RequestAborted); |
|||
|
|||
var response = Deferred.Response(() => |
|||
{ |
|||
return IndexesDto.FromDomain(indexes, Resources); |
|||
}); |
|||
|
|||
return Ok(response); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create a schema indexes.
|
|||
/// </summary>
|
|||
/// <param name="app">The name of the app.</param>
|
|||
/// <param name="schema">The name of the schema.</param>
|
|||
/// <param name="request">The request object that represents an index.</param>
|
|||
/// <response code="200">Schema findexes returned.</response>
|
|||
/// <response code="404">Schema or app not found.</response>
|
|||
[HttpPost] |
|||
[Route("apps/{app}/schemas/{schema}/indexes/")] |
|||
[ProducesResponseType(StatusCodes.Status204NoContent)] |
|||
[ApiPermissionOrAnonymous(PermissionIds.AppSchemasIndexes)] |
|||
[ApiCosts(1)] |
|||
public async Task<IActionResult> PostIndex(string app, string schema, [FromBody] CreateIndexDto request) |
|||
{ |
|||
var job = CreateIndexJob.BuildRequest(User.Token()!, App, Schema, request.ToIndex()); |
|||
|
|||
await jobService.StartAsync(App.Id, job, HttpContext.RequestAborted); |
|||
|
|||
return NoContent(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create a schema indexes.
|
|||
/// </summary>
|
|||
/// <param name="app">The name of the app.</param>
|
|||
/// <param name="schema">The name of the schema.</param>
|
|||
/// <param name="name">The name of the index.</param>
|
|||
/// <response code="204">Schema index deletion added to job queue.</response>
|
|||
/// <response code="404">Schema or app not found.</response>
|
|||
[HttpPost] |
|||
[Route("apps/{app}/schemas/{schema}/indexes/{name}")] |
|||
[ProducesResponseType(StatusCodes.Status204NoContent)] |
|||
[ApiPermissionOrAnonymous(PermissionIds.AppSchemasIndexes)] |
|||
[ApiCosts(1)] |
|||
public async Task<IActionResult> DeleteIndex(string app, string schema, string name) |
|||
{ |
|||
var job = DropIndexJob.BuildRequest(User.Token()!, App, Schema, name); |
|||
|
|||
await jobService.StartAsync(App.Id, job, HttpContext.RequestAborted); |
|||
|
|||
return NoContent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,172 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using FluentAssertions.Common; |
|||
using Jint.Runtime; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Jobs; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Collections; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Squidex.Infrastructure.States; |
|||
using Squidex.Infrastructure.TestHelpers; |
|||
using System.Security.Principal; |
|||
using IClock = NodaTime.IClock; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Indexes; |
|||
|
|||
public class CreateIndexJobTests : GivenContext |
|||
{ |
|||
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); |
|||
private readonly CreateIndexJob sut; |
|||
|
|||
public CreateIndexJobTests() |
|||
{ |
|||
sut = new CreateIndexJob(contentRepository); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_request() |
|||
{ |
|||
var job = |
|||
CreateIndexJob.BuildRequest(User, App, Schema, |
|||
[ |
|||
new IndexField("field1", SortOrder.Ascending), |
|||
new IndexField("field2", SortOrder.Descending), |
|||
]); |
|||
|
|||
job.Arguments.Should().BeEquivalentTo( |
|||
new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["schemaName"] = Schema.Name, |
|||
["field_field1"] = "Ascending", |
|||
["field_field2"] = "Descending" |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_arguments_do_not_contain_schemaId() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaName"] = Schema.Name, |
|||
["field_field1"] = "Ascending", |
|||
["field_field2"] = "Descending" |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await Assert.ThrowsAsync<DomainException>(() => sut.RunAsync(context, CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_arguments_do_not_contain_schemaName() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["field_field1"] = "Ascending", |
|||
["field_field2"] = "Descending" |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await Assert.ThrowsAsync<DomainException>(() => sut.RunAsync(context, CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_field_order_is_invalid() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["schemaName"] = Schema.Name, |
|||
["field_field1"] = "Invalid", |
|||
["field_field2"] = "Descending" |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await Assert.ThrowsAsync<DomainException>(() => sut.RunAsync(context, CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_fields_are_empty() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["schemaName"] = Schema.Name, |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await Assert.ThrowsAsync<DomainException>(() => sut.RunAsync(context, CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_invoke_content_repository() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["schemaName"] = Schema.Name, |
|||
["field_field1"] = "Ascending", |
|||
["field_field2"] = "Descending" |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
IndexDefinition? index = null; |
|||
|
|||
A.CallTo(() => contentRepository.CreateIndexAsync(App.Id, Schema.Id, A<IndexDefinition>._, CancellationToken)) |
|||
.Invokes(x => index = x.GetArgument<IndexDefinition>(2)); |
|||
|
|||
await sut.RunAsync(context, CancellationToken); |
|||
|
|||
index.Should().BeEquivalentTo( |
|||
[ |
|||
new IndexField("field1", SortOrder.Ascending), |
|||
new IndexField("field2", SortOrder.Descending) |
|||
]); |
|||
} |
|||
|
|||
private JobRunContext CreateContext(Job job) |
|||
{ |
|||
return new JobRunContext(null!, A.Fake<IClock>(), default) { Actor = User, Job = job, OwnerId = App.Id }; |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using FluentAssertions.Common; |
|||
using Jint.Runtime; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Jobs; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Collections; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Squidex.Infrastructure.States; |
|||
using IClock = NodaTime.IClock; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Indexes; |
|||
|
|||
public class DropIndexJobTests : GivenContext |
|||
{ |
|||
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); |
|||
private readonly DropIndexJob sut; |
|||
|
|||
public DropIndexJobTests() |
|||
{ |
|||
sut = new DropIndexJob(contentRepository); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_request() |
|||
{ |
|||
var job = DropIndexJob.BuildRequest(User, App, Schema, "MyIndex"); |
|||
|
|||
job.Arguments.Should().BeEquivalentTo( |
|||
new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["schemaName"] = Schema.Name, |
|||
["indexName"] = "MyIndex" |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_arguments_do_not_contain_schemaId() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaName"] = Schema.Name, |
|||
["indexName"] = "MyIndex" |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await Assert.ThrowsAsync<DomainException>(() => sut.RunAsync(context, CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_arguments_do_not_contain_schemaName() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["indexName"] = "MyIndex" |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await Assert.ThrowsAsync<DomainException>(() => sut.RunAsync(context, CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_arguments_do_not_contain_index_name() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["schemaName"] = Schema.Name, |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await Assert.ThrowsAsync<DomainException>(() => sut.RunAsync(context, CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_invoke_content_repository() |
|||
{ |
|||
var job = new Job |
|||
{ |
|||
Arguments = new Dictionary<string, string> |
|||
{ |
|||
["appId"] = App.Id.ToString(), |
|||
["appName"] = App.Name, |
|||
["schemaId"] = Schema.Id.ToString(), |
|||
["schemaName"] = Schema.Name, |
|||
["indexName"] = "MyIndex" |
|||
}.ToReadonlyDictionary() |
|||
}; |
|||
|
|||
var context = CreateContext(job); |
|||
|
|||
await sut.RunAsync(context, CancellationToken); |
|||
|
|||
A.CallTo(() => contentRepository.DropIndexAsync(App.Id, Schema.Id, "MyIndex", CancellationToken)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
private JobRunContext CreateContext(Job job) |
|||
{ |
|||
return new JobRunContext(null!, A.Fake<IClock>(), default) { Actor = User, Job = job, OwnerId = App.Id }; |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; |
|||
|
|||
public class AdaptionTests |
|||
{ |
|||
static AdaptionTests() |
|||
{ |
|||
MongoContentEntity.RegisterClassMap(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_adapt_to_meta_field() |
|||
{ |
|||
var source = "lastModified"; |
|||
|
|||
var result = Adapt.MapPath(source).ToString(); |
|||
|
|||
Assert.Equal("mt", result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_adapt_to_data_field() |
|||
{ |
|||
var source = "data.test"; |
|||
|
|||
var result = Adapt.MapPath(source).ToString(); |
|||
|
|||
Assert.Equal("do.test", result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_adapt_from_meta_field() |
|||
{ |
|||
var source = "mt"; |
|||
|
|||
var result = Adapt.MapPathReverse(source).ToString(); |
|||
|
|||
Assert.Equal("lastModified", result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_adapt_from_data_field() |
|||
{ |
|||
var source = "do.test"; |
|||
|
|||
var result = Adapt.MapPathReverse(source).ToString(); |
|||
|
|||
Assert.Equal("data.test", result); |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using MongoDB.Bson; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
using Squidex.Infrastructure.Queries; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; |
|||
|
|||
public class IndexParserTests |
|||
{ |
|||
private readonly BsonDocument validSource = |
|||
new BsonDocument |
|||
{ |
|||
["name"] = "custom_index", |
|||
["key"] = new BsonDocument |
|||
{ |
|||
["mt"] = 1, |
|||
["mb"] = -1, |
|||
["do.field1"] = 1, |
|||
} |
|||
}; |
|||
|
|||
static IndexParserTests() |
|||
{ |
|||
MongoContentEntity.RegisterClassMap(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_index() |
|||
{ |
|||
var result = IndexParser.TryParse(validSource, "custom_", out var definition); |
|||
|
|||
Assert.True(result); |
|||
|
|||
definition.Should().BeEquivalentTo( |
|||
new IndexDefinition() |
|||
{ |
|||
new IndexField("lastModified", SortOrder.Ascending), |
|||
new IndexField("lastModifiedBy", SortOrder.Descending), |
|||
new IndexField("data.field1", SortOrder.Ascending), |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_parse_index_if_prefix_does_not_match() |
|||
{ |
|||
var result = IndexParser.TryParse(validSource, "prefix_", out var definition); |
|||
|
|||
Assert.False(result); |
|||
Assert.Null(definition); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_parse_index_if_name_not_found() |
|||
{ |
|||
validSource.Remove("name"); |
|||
|
|||
var result = IndexParser.TryParse(validSource, "custom_", out var definition); |
|||
|
|||
Assert.False(result); |
|||
Assert.Null(definition); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_parse_index_if_name_has_invalid_type() |
|||
{ |
|||
validSource["name"] = 42; |
|||
|
|||
var result = IndexParser.TryParse(validSource, "custom_", out var definition); |
|||
|
|||
Assert.False(result); |
|||
Assert.Null(definition); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_parse_index_if_key_not_found() |
|||
{ |
|||
validSource.Remove("key"); |
|||
|
|||
var result = IndexParser.TryParse(validSource, "custom_", out var definition); |
|||
|
|||
Assert.False(result); |
|||
Assert.Null(definition); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_parse_index_if_key_has_invalid_type() |
|||
{ |
|||
validSource["key"] = 42; |
|||
|
|||
var result = IndexParser.TryParse(validSource, "custom_", out var definition); |
|||
|
|||
Assert.False(result); |
|||
Assert.Null(definition); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_parse_index_if_key_is_empty() |
|||
{ |
|||
validSource["key"] = new BsonDocument(); |
|||
|
|||
var result = IndexParser.TryParse(validSource, "custom_", out var definition); |
|||
|
|||
Assert.False(result); |
|||
Assert.Null(definition); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_parse_index_if_key_property_has_invalid_type() |
|||
{ |
|||
validSource["key"].AsBsonDocument["mt"] = "invalid"; |
|||
|
|||
var result = IndexParser.TryParse(validSource, "custom_", out var definition); |
|||
|
|||
Assert.False(result); |
|||
Assert.Null(definition); |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Queries; |
|||
|
|||
namespace Squidex.Infrastructure.States; |
|||
|
|||
public class IndexDefinitionTests |
|||
{ |
|||
[Fact] |
|||
public void Should_create_name_for_empty_definition() |
|||
{ |
|||
var definition = new IndexDefinition(); |
|||
|
|||
Assert.Equal(string.Empty, definition.ToName()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_name_for_asc_order() |
|||
{ |
|||
var definition = new IndexDefinition |
|||
{ |
|||
new IndexField("field1", SortOrder.Ascending) |
|||
}; |
|||
|
|||
Assert.Equal("field1_asc", definition.ToName()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_name_for_dasc_order() |
|||
{ |
|||
var definition = new IndexDefinition |
|||
{ |
|||
new IndexField("field1", SortOrder.Descending) |
|||
}; |
|||
|
|||
Assert.Equal("field1_desc", definition.ToName()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_name_for_multiple_fields() |
|||
{ |
|||
var definition = new IndexDefinition |
|||
{ |
|||
new IndexField("field1", SortOrder.Ascending), |
|||
new IndexField("field2", SortOrder.Descending) |
|||
}; |
|||
|
|||
Assert.Equal("field1_asc_field2_desc", definition.ToName()); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { FormsModule } from '@angular/forms'; |
|||
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; |
|||
import { RadioGroupComponent, ToggleComponent } from '@app/framework'; |
|||
|
|||
export default { |
|||
title: 'Framework/Toggle', |
|||
component: ToggleComponent, |
|||
argTypes: { |
|||
disabled: { |
|||
control: 'boolean', |
|||
}, |
|||
change: { |
|||
action:'ngModelChange', |
|||
}, |
|||
}, |
|||
render: args => ({ |
|||
props: args, |
|||
template: ` |
|||
<sqx-toggle |
|||
[disabled]="disabled" |
|||
(ngModelChange)="change($event)" |
|||
[ngModel]="model"> |
|||
</sqx-toggle> |
|||
`,
|
|||
}), |
|||
decorators: [ |
|||
moduleMetadata({ |
|||
imports: [ |
|||
FormsModule, |
|||
], |
|||
}), |
|||
], |
|||
} as Meta; |
|||
|
|||
type Story = StoryObj<RadioGroupComponent & { model: any }>; |
|||
|
|||
export const Default: Story = {}; |
|||
|
|||
export const Checked: Story = { |
|||
args: { |
|||
model: true, |
|||
}, |
|||
}; |
|||
|
|||
export const Unchecked: Story = { |
|||
args: { |
|||
model: false, |
|||
}, |
|||
}; |
|||
Loading…
Reference in new issue