mirror of https://github.com/Squidex/squidex.git
59 changed files with 1880 additions and 54 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,118 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Security.Claims; |
|||
using Jint; |
|||
using Jint.Native; |
|||
using Jint.Native.Object; |
|||
using Jint.Runtime; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Core.Scripting.Internal; |
|||
using Squidex.Domain.Apps.Entities.Properties; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents; |
|||
|
|||
public sealed class ContentsJintExtension : IJintExtension, IScriptDescriptor |
|||
{ |
|||
private delegate void GetContentsDelegate(string schema, JsValue query, Action<JsValue> callback); |
|||
private readonly IServiceProvider serviceProvider; |
|||
|
|||
public ContentsJintExtension(IServiceProvider serviceProvider) |
|||
{ |
|||
this.serviceProvider = serviceProvider; |
|||
} |
|||
|
|||
public void ExtendAsync(ScriptExecutionContext context) |
|||
{ |
|||
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!context.TryGetValueIfExists<ClaimsPrincipal>("user", out var user)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var getContents = new GetContentsDelegate((schemas, query, callback) => |
|||
{ |
|||
GetContents(context, appId, user, schemas, query, callback); |
|||
}); |
|||
|
|||
context.Engine.SetValue("getContents", getContents); |
|||
} |
|||
|
|||
private void GetContents(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, |
|||
string schema, JsValue query, Action<JsValue> callback) |
|||
{ |
|||
if (callback == null) |
|||
{ |
|||
throw new JavaScriptException("Callback is not defined."); |
|||
} |
|||
|
|||
context.Schedule(async (scheduler, ct) => |
|||
{ |
|||
var app = await GetAppAsync(appId); |
|||
|
|||
if (app == null) |
|||
{ |
|||
scheduler.Run(callback, new JsArray(context.Engine)); |
|||
return; |
|||
} |
|||
|
|||
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>(); |
|||
|
|||
var requestContext = |
|||
new Context(user, app).Clone(b => b |
|||
.WithFields(null) |
|||
.WithNoEnrichment() |
|||
.WithUnpublished() |
|||
.WithNoTotal()); |
|||
|
|||
var q = Q.Empty; |
|||
if (query is ObjectInstance obj) |
|||
{ |
|||
if (obj.TryGetValue("query", out var t) && t is JsString oDataQuery) |
|||
{ |
|||
q = q.WithODataQuery(oDataQuery.AsString()); |
|||
} |
|||
} |
|||
else if (query is JsString oDataQuery) |
|||
{ |
|||
q = q.WithODataQuery(oDataQuery.AsString()); |
|||
} |
|||
|
|||
var contents = await contentQuery.QueryAsync(requestContext, schema, q, ct); |
|||
|
|||
scheduler.Run(callback, JsValue.FromObject(context.Engine, contents.ToArray())); |
|||
}); |
|||
} |
|||
|
|||
private async Task<App> GetAppAsync(DomainId appId) |
|||
{ |
|||
var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); |
|||
|
|||
var app = await appProvider.GetAppAsync(appId) ?? |
|||
throw new JavaScriptException("App does not exist."); |
|||
|
|||
return app; |
|||
} |
|||
|
|||
public void Describe(AddDescription describe, ScriptScope scope) |
|||
{ |
|||
if (!scope.HasFlag(ScriptScope.Async)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
describe(JsonType.Function, "getContents(schema, query, callback)", |
|||
Resources.ScriptingGetContents); |
|||
} |
|||
} |
|||
@ -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,147 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Security.Claims; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents; |
|||
|
|||
public class ContentsJintExtensionTests : GivenContext, IClassFixture<TranslationsFixture> |
|||
{ |
|||
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); |
|||
private readonly JintScriptEngine sut; |
|||
|
|||
public ContentsJintExtensionTests() |
|||
{ |
|||
var serviceProvider = |
|||
new ServiceCollection() |
|||
.AddSingleton(AppProvider) |
|||
.AddSingleton(contentQuery) |
|||
.BuildServiceProvider(); |
|||
|
|||
var extensions = new IJintExtension[] |
|||
{ |
|||
new ContentsJintExtension(serviceProvider) |
|||
}; |
|||
|
|||
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), |
|||
Options.Create(new JintScriptOptions |
|||
{ |
|||
TimeoutScript = TimeSpan.FromSeconds(2), |
|||
TimeoutExecution = TimeSpan.FromSeconds(10) |
|||
}), |
|||
extensions); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_callback_is_null() |
|||
{ |
|||
var (vars, _) = SetupQueryVars("my-schema", "$filter=data/field/iv eq 42", 2); |
|||
|
|||
var script = @"getContents('my-schema', '$filter=data/field/iv eq 42')"; |
|||
|
|||
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_contents() |
|||
{ |
|||
var (vars, _) = SetupQueryVars("my-schema", "$filter=data/field/iv eq 42", 2); |
|||
|
|||
var expected = @"
|
|||
Text: Hello 1 World 1 |
|||
";
|
|||
|
|||
var script = @"
|
|||
getContents('my-schema', { query: '$filter=data/field/iv eq 42' }, function (references) { |
|||
var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; |
|||
|
|||
complete(`${actual1}`); |
|||
})";
|
|||
|
|||
var actual = (await sut.ExecuteAsync(vars, script, ct: CancellationToken)).ToString(); |
|||
|
|||
Assert.Equal(Cleanup(expected), Cleanup(actual)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_contents_with_string() |
|||
{ |
|||
var (vars, _) = SetupQueryVars("my-schema", "$filter=data/field/iv eq 42", 2); |
|||
|
|||
var expected = @"
|
|||
Text: Hello 1 World 1 |
|||
";
|
|||
|
|||
var script = @"
|
|||
getContents('my-schema', '$filter=data/field/iv eq 42', function (references) { |
|||
var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; |
|||
|
|||
complete(`${actual1}`); |
|||
})";
|
|||
|
|||
var actual = (await sut.ExecuteAsync(vars, script, ct: CancellationToken)).ToString(); |
|||
|
|||
Assert.Equal(Cleanup(expected), Cleanup(actual)); |
|||
} |
|||
|
|||
private (ScriptVars, EnrichedContent[]) SetupQueryVars(string schema, string filter, int count) |
|||
{ |
|||
var references = Enumerable.Range(0, count).Select((x, i) => CreateContent(i + 1)).ToArray(); |
|||
var referenceIds = references.Select(x => x.Id); |
|||
|
|||
var user = new ClaimsPrincipal(); |
|||
|
|||
A.CallTo(() => contentQuery.QueryAsync( |
|||
A<Context>.That.Matches(x => x.App == App && x.UserPrincipal == user), |
|||
schema, |
|||
A<Q>.That.Matches(x => x.QueryAsOdata == filter), |
|||
A<CancellationToken>._)) |
|||
.Returns(ResultList.CreateFrom(2, [CreateContent(1)])); |
|||
|
|||
var vars = new ScriptVars |
|||
{ |
|||
["appId"] = AppId.Id, |
|||
["appName"] = AppId.Name, |
|||
["user"] = user |
|||
}; |
|||
|
|||
return (vars, references); |
|||
} |
|||
|
|||
private EnrichedContent CreateContent(int index) |
|||
{ |
|||
return CreateContent() with |
|||
{ |
|||
Data = |
|||
new ContentData() |
|||
.AddField("field1", |
|||
new ContentFieldData() |
|||
.AddInvariant(JsonValue.Create($"Hello {index}"))) |
|||
.AddField("field2", |
|||
new ContentFieldData() |
|||
.AddInvariant(JsonValue.Create($"World {index}"))) |
|||
}; |
|||
} |
|||
|
|||
private static string Cleanup(string text) |
|||
{ |
|||
return text |
|||
.Replace("\r", string.Empty, StringComparison.Ordinal) |
|||
.Replace("\n", string.Empty, StringComparison.Ordinal) |
|||
.Replace(" ", string.Empty, StringComparison.Ordinal); |
|||
} |
|||
} |
|||
@ -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