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