Browse Source

Pre query. (#831)

* Pre query.

* fix tests

* Number

* Fix tests?

* Remove old test.
pull/832/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
a8b0ada3ee
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs
  3. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  4. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  5. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  6. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs
  7. 9
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  8. 44
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs
  9. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  10. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs
  11. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs
  12. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs
  13. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  14. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs
  15. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs
  16. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs
  17. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs
  18. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs
  19. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs
  20. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs
  21. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  23. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs
  24. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs
  25. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs
  26. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs
  27. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs
  28. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  29. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs
  30. 28
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs
  31. 48
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  32. 30
      backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingCompletion.cs
  33. 24
      backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs
  34. 9
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs
  35. 105
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs
  36. 184
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  37. 207
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  38. 30
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs
  39. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs
  40. 82
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  41. 98
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs
  42. 18
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs
  43. 69
      backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs
  44. 13
      backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs
  45. 97
      backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs
  46. 30
      backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs
  47. 164
      backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs
  48. 222
      backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs
  49. 6
      backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs
  50. 2
      backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
  51. 10
      backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs
  52. 2
      backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj
  53. 3
      backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs
  54. 8
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture1to10.cs
  55. 32
      backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs
  56. 6
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
  57. 1
      frontend/src/app/features/schemas/declarations.ts
  58. 3
      frontend/src/app/features/schemas/module.ts
  59. 2
      frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.html
  60. 24
      frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts.pipes.spec.ts
  61. 22
      frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts.pipes.ts
  62. 3
      frontend/src/app/shared/state/schemas.forms.ts

2
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs

@ -27,7 +27,7 @@ namespace Squidex.Extensions.Validation
this.contentRepository = contentRepository;
}
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is ContentData data)
{

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs

@ -20,5 +20,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string? Delete { get; init; }
public string? Query { get; init; }
public string? QueryPre { get; init; }
}
}

10
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -143,5 +143,15 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
}
}
}
public override object ToObject()
{
if (TryUpdate(out var result))
{
return result;
}
return contentData;
}
}
}

10
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs

@ -161,5 +161,15 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
}
}
}
public override object ToObject()
{
if (TryUpdate(out var result))
{
return result!;
}
return fieldData!;
}
}
}

5
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs

@ -17,6 +17,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
}
public ScriptContext(ScriptContext source)
: base(source, StringComparer.OrdinalIgnoreCase)
{
}
public bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value)
{
Guard.NotNull(key, nameof(key));

13
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs

@ -68,18 +68,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
if (options.AsContext)
{
var contextInstance = new ObjectInstance(engine);
var contextInstance = new WritableContext(engine, vars);
foreach (var (key, value) in vars)
foreach (var (key, value) in vars.Where(x => x.Value != null))
{
var property = key.ToCamelCase();
if (value != null)
{
contextInstance.FastAddProperty(property, JsValue.FromObject(engine, value), true, true, true);
this[property] = value;
}
this[key.ToCamelCase()] = value;
}
engine.SetValue("ctx", contextInstance);

9
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs

@ -12,6 +12,15 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptVars : ScriptContext
{
public ScriptVars()
{
}
public ScriptVars(ScriptVars source)
: base(source)
{
}
public ContentData? Data
{
get => GetValue<ContentData?>();

44
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Squidex.Text;
namespace Squidex.Domain.Apps.Core.Scripting
{
internal class WritableContext : ObjectInstance
{
private readonly ScriptVars vars;
public WritableContext(Engine engine, ScriptVars vars)
: base(engine)
{
this.vars = vars;
foreach (var (key, value) in vars)
{
var property = key.ToCamelCase();
if (value != null)
{
FastAddProperty(property, FromObject(engine, value), true, true, true);
}
}
}
public override bool Set(JsValue property, JsValue value, JsValue receiver)
{
var propertyName = property.AsString();
vars[propertyName] = value.ToObject();
return base.Set(property, value, receiver);
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
errors.Add(new ValidationError(message, pathString));
}
public Task ValidateInputPartialAsync(ContentData data)
public ValueTask ValidateInputPartialAsync(ContentData data)
{
Guard.NotNull(data, nameof(data));
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return validator.ValidateAsync(data, context, AddError);
}
public Task ValidateInputAsync(ContentData data)
public ValueTask ValidateInputAsync(ContentData data)
{
Guard.NotNull(data, nameof(data));
@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return validator.ValidateAsync(data, context, AddError);
}
public Task ValidateContentAsync(ContentData data)
public ValueTask ValidateContentAsync(ContentData data)
{
Guard.NotNull(data, nameof(data));

2
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs

@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public interface IValidator
{
Task ValidateAsync(object? value, ValidationContext context, AddError addError);
ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError);
}
}

5
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Translations;
using Squidex.Log;
@ -22,13 +23,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.log = log;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
try
{
if (validators?.Length > 0)
{
await Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError)));
await AsyncHelper.WhenAllThrottledAsync(validators, (x, _) => x.ValidateAsync(value, context, addError));
}
}
catch (Exception ex)

8
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -26,14 +26,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.allowedValues = allowedValues;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is TValue typedValue && !allowedValues.Contains(typedValue))
{
addError(context.Path, T.Get("contents.validation.notAllowed"));
}
return Task.CompletedTask;
return default;
}
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs

@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.checkAssets = checkAssets;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
var foundIds = new List<DomainId>();

21
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs

@ -7,6 +7,7 @@
using System.Collections;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
@ -21,23 +22,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.itemValidator = itemValidator;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is ICollection { Count: > 0 } items)
{
var innerTasks = new List<Task>();
var index = 1;
foreach (var item in items)
var targets = items.OfType<object>().Select((item, index) =>
{
var innerContext = context.Nested($"[{index}]");
await itemValidator.ValidateAsync(item, innerContext, addError);
var innerContext = context.Nested($"[{index + 1}]");
index++;
}
return (item, innerContext);
});
await Task.WhenAll(innerTasks);
await AsyncHelper.WhenAllThrottledAsync(targets, async (x, _) =>
{
await itemValidator.ValidateAsync(x.item, x.innerContext, addError);
});
}
}
}

10
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.maxItems = maxItems;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is not ICollection items || items.Count == 0)
{
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
addError(context.Path, T.Get("contents.validation.required"));
}
return Task.CompletedTask;
return default;
}
if (minItems != null && maxItems != null)
@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
return Task.CompletedTask;
return default;
}
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.validatorFactory = validatorFactory;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is Component component)
{

12
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs

@ -14,19 +14,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class FieldValidator : IValidator
{
private readonly IValidator fieldValueValidator;
private readonly IValidator validator;
private readonly IField field;
public FieldValidator(IValidator fieldValueValidator, IField field)
public FieldValidator(IValidator validator, IField field)
{
Guard.NotNull(field, nameof(field));
Guard.NotNull(fieldValueValidator, nameof(fieldValueValidator));
Guard.NotNull(validator, nameof(validator));
this.field = field;
this.fieldValueValidator = fieldValueValidator;
this.validator = validator;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
var typedValue = value;
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
return;
}
await fieldValueValidator.ValidateAsync(typedValue, context, addError);
await validator.ValidateAsync(typedValue, context, addError);
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -17,14 +17,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (!value.IsUndefined())
{
addError(context.Path, T.Get("contents.validation.mustBeEmpty"));
}
return Task.CompletedTask;
return default;
}
}
}

21
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
@ -24,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.isPartial = isPartial;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value.IsNullOrUndefined())
{
@ -43,19 +44,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
var tasks = new List<Task>();
foreach (var (fieldName, fieldConfig) in fields)
await AsyncHelper.WhenAllThrottledAsync(fields, async (kvp, _) =>
{
var (isOptional, validator) = fieldConfig;
var (isOptional, validator) = kvp.Value;
var fieldValue = Undefined.Value;
if (!values.TryGetValue(fieldName, out var nestedValue))
if (!values.TryGetValue(kvp.Key, out var nestedValue))
{
if (isPartial)
{
continue;
return;
}
}
else
@ -63,12 +62,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
fieldValue = nestedValue!;
}
var fieldContext = context.Nested(fieldName).Optional(isOptional);
tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError));
}
var fieldContext = context.Nested(kvp.Key).Optional(isOptional);
await Task.WhenAll(tasks);
await validator.ValidateAsync(fieldValue, fieldContext, addError);
});
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs

@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
regex = new Regex($"^{pattern}$", options, Timeout);
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is string stringValue)
{
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
return Task.CompletedTask;
return default;
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.max = max;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is TValue typedValue)
{
@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
return Task.CompletedTask;
return default;
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.checkReferences = checkReferences;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
var foundIds = new List<DomainId>();

8
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -18,11 +18,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.validateEmptyStrings = validateEmptyStrings;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (context.IsOptional)
{
return Task.CompletedTask;
return default;
}
if (value.IsNullOrUndefined() || IsEmptyString(value))
@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
addError(context.Path, T.Get("contents.validation.required"));
}
return Task.CompletedTask;
return default;
}
private bool IsEmptyString(object? value)

8
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -11,14 +11,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public class RequiredValidator : IValidator
{
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value.IsNullOrUndefined() && !context.IsOptional)
{
addError(context.Path, T.Get("contents.validation.required"));
}
return Task.CompletedTask;
return default;
}
}
}
}

8
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.maxLength = maxLength;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is string stringValue && !string.IsNullOrEmpty(stringValue))
{
@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
return Task.CompletedTask;
return default;
}
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs

@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.maxWords = maxWords;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is string stringValue && !string.IsNullOrEmpty(stringValue))
{
@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
return Task.CompletedTask;
return default;
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.fields = fields;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is IEnumerable<JsonObject> objects && objects.Count() > 1)
{
@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
Validate(context, addError, components.Select(x => x.Data));
}
return Task.CompletedTask;
return default;
}
private void Validate(ValidationContext context, AddError addError, IEnumerable<JsonObject> items)

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.checkUniqueness = checkUniqueness;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
var count = context.Path.Count();

8
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class UniqueValuesValidator<TValue> : IValidator
{
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is IEnumerable<TValue> items && items.Any())
{
@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
}
return Task.CompletedTask;
return default;
}
}
}
}

28
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs

@ -37,25 +37,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas,
CancellationToken ct)
{
if (ShouldEnrich(context))
if (!ShouldEnrich(context))
{
var ids = new HashSet<DomainId>();
return;
}
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var (schema, components) = await schemas(group.Key);
var ids = new HashSet<DomainId>();
AddReferenceIds(ids, schema, components, group);
}
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var (schema, components) = await schemas(group.Key);
var references = await GetReferencesAsync(context, ids, ct);
AddReferenceIds(ids, schema, components, group);
}
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var (schema, components) = await schemas(group.Key);
var references = await GetReferencesAsync(context, ids, ct);
await ResolveReferencesAsync(context, schema, components, group, references, schemas);
}
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var (schema, components) = await schemas(group.Key);
await ResolveReferencesAsync(context, schema, components, group, references, schemas);
}
}

48
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
@ -31,32 +32,55 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas,
CancellationToken ct)
{
if (ShouldEnrich(context))
if (!ShouldEnrich(context))
{
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
return;
}
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var (schema, _) = await schemas(group.Key);
var script = schema.SchemaDef.Scripts.Query;
if (string.IsNullOrWhiteSpace(script))
{
continue;
}
var vars = new ScriptVars
{
var (schema, _) = await schemas(group.Key);
[ScriptKeys.AppId] = context.App.Id,
[ScriptKeys.AppName] = context.App.Name,
[ScriptKeys.User] = context.User
};
var script = schema.SchemaDef.Scripts.Query;
var preScript = schema.SchemaDef.Scripts.QueryPre;
if (!string.IsNullOrWhiteSpace(script))
if (!string.IsNullOrWhiteSpace(preScript))
{
var options = new ScriptOptions
{
await Task.WhenAll(group.Select(x => TransformAsync(context, script, x, ct)));
}
AsContext = true
};
await scriptEngine.ExecuteAsync(vars, preScript, options, ct);
}
await AsyncHelper.WhenAllThrottledAsync(group, async (content, _) =>
{
await TransformAsync(vars, script, content, ct);
}, ct: ct);
}
}
private async Task TransformAsync(Context context, string script, ContentEntity content,
private async Task TransformAsync(ScriptVars rootVars, string script, ContentEntity content,
CancellationToken ct)
{
var vars = new ScriptVars
var vars = new ScriptVars(rootVars)
{
[ScriptKeys.AppId] = context.App.Id,
[ScriptKeys.AppName] = context.App.Name,
[ScriptKeys.ContentId] = content.Id,
[ScriptKeys.Data] = content.Data,
[ScriptKeys.User] = context.User
};
var options = new ScriptOptions

30
backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingCompletion.cs

@ -17,25 +17,25 @@ namespace Squidex.Domain.Apps.Entities.Scripting
public IReadOnlyList<ScriptingValue> Content(Schema schema, PartitionResolver partitionResolver)
{
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddFunction("replace()",
"Tell Squidex that you have modified the data and that the change should be applied.");
AddFunction("replace()",
"Tell Squidex that you have modified the data and that the change should be applied.");
AddFunction("getReferences(ids, callback)",
"Queries the content items with the specified IDs and invokes the callback with an array of contents.");
AddFunction("getReferences(ids, callback)",
"Queries the content items with the specified IDs and invokes the callback with an array of contents.");
AddFunction("getReference(ids, callback)",
"Queries the content item with the specified ID and invokes the callback with an array of contents.");
AddFunction("getReference(ids, callback)",
"Queries the content item with the specified ID and invokes the callback with an array of contents.");
AddFunction("getAssets(ids, callback)",
"Queries the assets with the specified IDs and invokes the callback with an array of assets.");
AddFunction("getAssets(ids, callback)",
"Queries the assets with the specified IDs and invokes the callback with an array of assets.");
AddFunction("getAsset(ids, callback)",
"Queries the asset with the specified ID and invokes the callback with an array of assets.");
AddFunction("getAsset(ids, callback)",
"Queries the asset with the specified ID and invokes the callback with an array of assets.");
AddShared();
AddShared();
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddString("contentId",
FieldDescriptions.EntityId);
@ -61,10 +61,10 @@ namespace Squidex.Domain.Apps.Entities.Scripting
public IReadOnlyList<ScriptingValue> Asset()
{
AddShared();
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddShared();
AddString("assetId",
FieldDescriptions.EntityId);

24
backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs

@ -51,6 +51,30 @@ namespace Squidex.Infrastructure.Tasks
.GetResult();
}
public static async ValueTask WhenAllThrottledAsync<T>(IEnumerable<T> source, Func<T, CancellationToken, ValueTask> action, int maxDegreeOfParallelism = 0,
CancellationToken ct = default)
{
if (maxDegreeOfParallelism <= 0)
{
maxDegreeOfParallelism = Environment.ProcessorCount * 2;
}
var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
foreach (var item in source)
{
await semaphore.WaitAsync(ct);
try
{
await action(item, ct);
}
finally
{
semaphore.Release();
}
}
}
public static void Batch<TIn, TOut>(this Channel<object> source, Channel<TOut> target, Func<IReadOnlyList<TIn>, TOut> converter, int batchSize, int timeout,
CancellationToken ct = default)
{

9
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs

@ -14,10 +14,15 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
public sealed class SchemaScriptsDto
{
/// <summary>
/// The script that is executed for each query when querying contents.
/// The script that is executed for each content when querying contents.
/// </summary>
public string? Query { get; set; }
/// <summary>
/// The script that is executed for all contents when querying contents.
/// </summary>
public string? QueryPre { get; set; }
/// <summary>
/// The script that is executed when creating a content.
/// </summary>
@ -45,4 +50,4 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
return new ConfigureScripts { Scripts = scripts };
}
}
}
}

105
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs

@ -27,7 +27,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(1.0));
var result = ExecuteScript(original, @"data.number = { iv: 1 }");
const string script = @"
data.number = { iv: 1 }
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -43,7 +47,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(1.0));
var result = ExecuteScript(original, @"data.number.iv = 1");
const string script = @"
data.number.iv = 1
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -59,7 +67,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(1.0));
var result = ExecuteScript(original, "Object.defineProperty(data, 'number', { value: { iv: 1 } })");
const string script = @"
Object.defineProperty(data, 'number', { value: { iv: 1 } })
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -69,7 +81,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var original = new ContentData();
Assert.Throws<JavaScriptException>(() => ExecuteScript(original, @"data.number = 1"));
const string script = @"
data.number = 1
";
Assert.Throws<JavaScriptException>(() => ExecuteScript(original, script));
}
[Fact]
@ -83,7 +99,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
var expected = new ContentData();
var result = ExecuteScript(original, @"delete data.number");
const string script = @"
delete data.number
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -103,7 +123,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant("1new"));
var result = ExecuteScript(original, @"data.string.iv = data.string.iv + 'new'");
const string script = @"
data.string.iv = data.string.iv + 'new'
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -123,7 +147,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(3.0));
var result = ExecuteScript(original, @"data.number.iv = data.number.iv + 2");
const string script = @"
data.number.iv = data.number.iv + 2
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -143,7 +171,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(true));
var result = ExecuteScript(original, @"data.boolean.iv = !data.boolean.iv");
const string script = @"
data.boolean.iv = !data.boolean.iv
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -163,7 +195,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(JsonValue.Array(1.0, 4.0, 5.0)));
var result = ExecuteScript(original, @"data.number.iv = [data.number.iv[0], data.number.iv[1] + 2, 5]");
const string script = @"
data.number.iv = [data.number.iv[0], data.number.iv[1] + 2, 5]
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -183,7 +219,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(JsonValue.Object().Add("lat", 1.0).Add("lon", 4.0)));
var result = ExecuteScript(original, @"data.geo.iv = { lat: data.geo.iv.lat, lon: data.geo.iv.lat + 3 }");
const string script = @"
data.geo.iv = { lat: data.geo.iv.lat, lon: data.geo.iv.lat + 3 }
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -203,11 +243,14 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(JsonValue.Object().Add("lat", 2.0).Add("lon", 4.0)));
var result = ExecuteScript(original, @"
const string script = @"
var nested = data.geo.iv;
nested.lat = 2;
nested.lon = 4;
data.geo.iv = nested");
data.geo.iv = nested
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -221,11 +264,14 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(JsonValue.Object().Add("lat", 2.0).Add("lon", 4.0)));
var result = ExecuteScript(original, @"
const string script = @"
var nested = data.geo.iv;
nested.lat = 2;
nested.lon = 4;
data.geo.iv = nested");
data.geo.iv = nested
";
var result = ExecuteScript(original, script);
Assert.Same(original, result);
}
@ -244,7 +290,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(1.0));
var result = ExecuteScript(original, "Object.defineProperty(data.number, 'iv', { value: 1 })");
const string script = @"
Object.defineProperty(data.number, 'iv', { value: 1 })
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -263,7 +313,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
.AddField("string",
new ContentFieldData());
var result = ExecuteScript(original, @"delete data.string.iv");
const string script = @"
delete data.string.iv
";
var result = ExecuteScript(original, script);
Assert.Equal(expected, result);
}
@ -286,7 +340,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
engine.SetValue("data", new ContentDataObject(engine, content));
var result = engine.Evaluate(@"
const string script = @"
var result = [];
for (var x in data) {
var field = data[x];
@ -295,7 +349,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
result.push(field[y]);
}
}
result;").ToObject();
result;
";
var result = engine.Evaluate(script).ToObject();
Assert.Equal(new[] { "1", "2", "3", "4" }, result);
}
@ -309,13 +366,21 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(JsonValue.Array()));
ExecuteScript(original, "data.obj.iv[0] = 1");
const string script = @"
data.obj.iv[0] = 1
";
ExecuteScript(original, script);
}
[Fact]
public void Should_null_propagate_unknown_fields()
{
ExecuteScript(new ContentData(), @"data.string.iv = 'hello'");
const string script = @"
data.string.iv = 'hello'
";
ExecuteScript(new ContentData(), script);
}
private static ContentData ExecuteScript(ContentData original, string script)

184
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -47,15 +47,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_convert_html_to_text()
{
const string script = @"
return html2Text(value);
";
var vars = new ScriptVars
{
["value"] = "<script>Invalid</script><STYLE>Invalid</STYLE><p>Hello World</p>"
};
const string script = @"
return html2Text(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("Hello World", result);
@ -64,15 +64,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_convert_markdown_to_text()
{
const string script = @"
return markdown2Text(value);
";
var vars = new ScriptVars
{
["value"] = "## Hello World"
};
const string script = @"
return markdown2Text(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("Hello World", result);
@ -81,15 +81,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_count_words()
{
const string script = @"
return wordCount(value);
";
var vars = new ScriptVars
{
["value"] = "Hello, World"
};
const string script = @"
return wordCount(value);
";
var result = ((JsonNumber)sut.Execute(vars, script)).Value;
Assert.Equal(2, result);
@ -98,15 +98,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_count_characters()
{
const string script = @"
return characterCount(value);
";
var vars = new ScriptVars
{
["value"] = "Hello, World"
};
const string script = @"
return characterCount(value);
";
var result = ((JsonNumber)sut.Execute(vars, script)).Value;
Assert.Equal(10, result);
@ -115,15 +115,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_camel_case_value()
{
const string script = @"
return toCamelCase(value);
";
var vars = new ScriptVars
{
["value"] = "Hello World"
};
const string script = @"
return toCamelCase(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("helloWorld", result);
@ -132,15 +132,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_pascal_case_value()
{
const string script = @"
return toPascalCase(value);
";
var vars = new ScriptVars
{
["value"] = "Hello World"
};
const string script = @"
return toPascalCase(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("HelloWorld", result);
@ -149,15 +149,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_slugify_value()
{
const string script = @"
return slugify(value);
";
var vars = new ScriptVars
{
["value"] = "4 Häuser"
};
const string script = @"
return slugify(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("4-haeuser", result);
@ -166,15 +166,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_slugify_value_with_single_char()
{
const string script = @"
return slugify(value, true);
";
var vars = new ScriptVars
{
["value"] = "4 Häuser"
};
const string script = @"
return slugify(value, true);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("4-hauser", result);
@ -183,15 +183,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_compute_sha256_hash()
{
const string script = @"
return sha256(value);
";
var vars = new ScriptVars
{
["value"] = "HelloWorld"
};
const string script = @"
return sha256(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("HelloWorld".ToSha256(), result);
@ -200,15 +200,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_compute_sha512_hash()
{
const string script = @"
return sha512(value);
";
var vars = new ScriptVars
{
["value"] = "HelloWorld"
};
const string script = @"
return sha512(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("HelloWorld".ToSha512(), result);
@ -217,15 +217,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_compute_md5_hash()
{
const string script = @"
return md5(value);
";
var vars = new ScriptVars
{
["value"] = "HelloWorld"
};
const string script = @"
return md5(value);
";
var result = sut.Execute(vars, script).ToString();
Assert.Equal("HelloWorld".ToMD5(), result);
@ -234,14 +234,14 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Should_compute_guid()
{
const string script = @"
return guid();
";
var vars = new ScriptVars
{
};
const string script = @"
return guid();
";
var result = sut.Execute(vars, script).ToString();
Assert.True(Guid.TryParse(result, out _));
@ -250,16 +250,18 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task Should_throw_validation_exception_if_calling_reject()
{
const string script = @"
reject()
";
var options = new ScriptOptions
{
CanReject = true
};
var vars = new ScriptVars();
var vars = new ScriptVars
{
};
const string script = @"
reject()
";
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options));
@ -269,16 +271,18 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task Should_throw_validation_exception_if_calling_reject_with_message()
{
const string script = @"
reject('Not valid')
";
var options = new ScriptOptions
{
CanReject = true
};
var vars = new ScriptVars();
var vars = new ScriptVars
{
};
const string script = @"
reject('Not valid')
";
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options));
@ -288,16 +292,18 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task Should_throw_security_exception_if_calling_reject()
{
const string script = @"
disallow()
";
var options = new ScriptOptions
{
CanDisallow = true
};
var vars = new ScriptVars();
var vars = new ScriptVars
{
};
const string script = @"
disallow()
";
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options));
@ -316,7 +322,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanDisallow = true
};
var vars = new ScriptVars();
var vars = new ScriptVars
{
};
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options));
@ -326,28 +334,32 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task Should_throw_exception_if_getJson_url_is_null()
{
var vars = new ScriptVars
{
};
const string script = @"
getJSON(null, function(result) {
complete(result);
});
";
var vars = new ScriptVars();
await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script));
}
[Fact]
public async Task Should_throw_exception_if_getJson_callback_is_null()
{
var vars = new ScriptVars
{
};
const string script = @"
var url = 'http://squidex.io';
getJSON(url, null);
";
var vars = new ScriptVars();
await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script));
}
@ -356,6 +368,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var httpHandler = SetupRequest();
var vars = new ScriptVars
{
};
const string script = @"
var url = 'http://squidex.io';
@ -364,8 +380,6 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
});
";
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
@ -381,6 +395,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var httpHandler = SetupRequest();
var vars = new ScriptVars
{
};
const string script = @"
var headers = {
'X-Header1': 1,
@ -394,8 +412,6 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
}, headers);
";
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
@ -413,6 +429,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var httpHandler = SetupRequest();
var vars = new ScriptVars
{
};
const string script = @"
var url = 'http://squidex.io';
@ -421,8 +441,6 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
});
";
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Delete);
@ -438,6 +456,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var httpHandler = SetupRequest();
var vars = new ScriptVars
{
};
const string script = @"
var url = 'http://squidex.io';
@ -448,8 +470,6 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
});
";
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Patch);
@ -466,6 +486,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var httpHandler = SetupRequest();
var vars = new ScriptVars
{
};
const string script = @"
var url = 'http://squidex.io';
@ -476,8 +500,6 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
});
";
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Post);
@ -494,6 +516,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var httpHandler = SetupRequest();
var vars = new ScriptVars
{
};
const string script = @"
var url = 'http://squidex.io';
@ -504,8 +530,6 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
});
";
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Put);

207
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -13,14 +13,16 @@ using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
public class JintScriptEngineTests
public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
{
private readonly ScriptOptions contentOptions = new ScriptOptions
{
@ -85,13 +87,17 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_return_original_content_if_script_failed()
{
var content = new ContentData();
var context = new ScriptVars { ["data"] = content };
var vars = new ScriptVars
{
["data"] = content
};
const string script = @"
x => x
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Empty(result);
}
@ -116,7 +122,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(10.0));
var context = new ScriptVars { ["data"] = content };
var vars = new ScriptVars
{
["data"] = content
};
const string script = @"
var data = ctx.data;
@ -129,7 +138,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(data);
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Equal(expected, result);
}
@ -147,27 +156,31 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_throw_exception_if_script_failed()
{
var content = new ContentData();
var context = new ScriptVars { ["data"] = content };
var vars = new ScriptVars
{
["data"] = new ContentData()
};
const string script = @"
invalid(();
";
await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(context, script, contentOptions));
await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(vars, script, contentOptions));
}
[Fact]
public async Task TransformAsync_should_return_original_content_if_not_replaced()
{
var content = new ContentData();
var context = new ScriptVars { ["data"] = content };
var vars = new ScriptVars
{
["data"] = new ContentData()
};
const string script = @"
var x = 0;
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Empty(result);
}
@ -175,8 +188,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_return_original_content_if_not_replaced_async()
{
var content = new ContentData();
var context = new ScriptVars { ["data"] = content };
var vars = new ScriptVars
{
["data"] = new ContentData()
};
const string script = @"
async = true;
@ -188,7 +203,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
});
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Empty(result);
}
@ -204,7 +219,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant("MyOperation"));
var context = new ScriptVars
var vars = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
@ -219,7 +234,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(data);
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Equal(expected, result);
}
@ -235,7 +250,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(42));
var context = new ScriptVars
var vars = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
@ -255,7 +270,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Equal(expected, result);
}
@ -263,10 +278,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_not_ignore_transformation_if_async_not_set()
{
var content = new ContentData();
var context = new ScriptVars
var vars = new ScriptVars
{
["data"] = content,
["data"] = new ContentData(),
["dataOld"] = null,
["operation"] = "MyOperation"
};
@ -282,7 +296,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.NotEmpty(result);
}
@ -290,10 +304,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_timeout_if_replace_never_called()
{
var content = new ContentData();
var context = new ScriptVars
var vars = new ScriptVars
{
["data"] = content,
["data"] = new ContentData(),
["dataOld"] = null,
["operation"] = "MyOperation"
};
@ -308,7 +321,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
});
";
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => sut.TransformAsync(context, script, contentOptions));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => sut.TransformAsync(vars, script, contentOptions));
}
[Fact]
@ -331,7 +344,10 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(10.0));
var context = new ScriptVars { ["data"] = content };
var vars = new ScriptVars
{
["data"] = content
};
const string script = @"
var data = ctx.data;
@ -344,7 +360,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(data);
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Equal(expected, result);
}
@ -375,7 +391,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2"));
var context = new ScriptVars
var vars = new ScriptVars
{
["data"] = content,
["dataOld"] = oldContent,
@ -388,7 +404,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(ctx.data);
";
var result = await sut.TransformAsync(context, script, contentOptions);
var result = await sut.TransformAsync(vars, script, contentOptions);
Assert.Equal(expected, result);
}
@ -396,16 +412,16 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Evaluate_should_return_true_if_expression_match()
{
const string script = @"
value.i == 2
";
var context = new ScriptVars
var vars = new ScriptVars
{
["value"] = new { i = 2 }
};
var result = ((IScriptEngine)sut).Evaluate(context, script);
const string script = @"
value.i == 2
";
var result = ((IScriptEngine)sut).Evaluate(vars, script);
Assert.True(result);
}
@ -413,16 +429,16 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Evaluate_should_return_true_if_status_match()
{
const string script = @"
value.status == 'Published'
";
var context = new ScriptVars
var vars = new ScriptVars
{
["value"] = new { status = Status.Published }
};
var result = ((IScriptEngine)sut).Evaluate(context, script);
const string script = @"
value.status == 'Published'
";
var result = ((IScriptEngine)sut).Evaluate(vars, script);
Assert.True(result);
}
@ -430,16 +446,16 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Evaluate_should_return_false_if_expression_match()
{
const string script = @"
value.i == 3
";
var context = new ScriptVars
var vars = new ScriptVars
{
["value"] = new { i = 2 }
};
var result = ((IScriptEngine)sut).Evaluate(context, script);
const string script = @"
value.i == 3
";
var result = ((IScriptEngine)sut).Evaluate(vars, script);
Assert.False(result);
}
@ -447,16 +463,16 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public void Evaluate_should_return_false_if_script_is_invalid()
{
const string script = @"
function();
";
var context = new ScriptVars
var vars = new ScriptVars
{
["value"] = new { i = 2 }
};
var result = ((IScriptEngine)sut).Evaluate(context, script);
const string script = @"
function();
";
var result = ((IScriptEngine)sut).Evaluate(vars, script);
Assert.False(result);
}
@ -466,18 +482,93 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var id = DomainId.NewGuid();
var vars = new ScriptVars
{
["value"] = id
};
const string script = @"
return value;
";
var context = new ScriptVars
var result = sut.Execute(vars, script);
Assert.Equal(id.ToString(), result.ToString());
}
[Fact]
public void Should_share_vars_between_executions()
{
var vars = new ScriptVars
{
["value"] = id
["value"] = 13
};
var result = sut.Execute(context, script);
const string script1 = @"
ctx.value = ctx.value * 2;
";
Assert.Equal(id.ToString(), result.ToString());
const string script2 = @"
return ctx.value + 2;
";
sut.Execute(vars, script1, new ScriptOptions { AsContext = true });
var result = sut.Execute(vars, script2, new ScriptOptions { AsContext = true });
Assert.Equal(JsonValue.Create(28), result);
}
[Fact]
public void Should_share_complex_vars_between_executions()
{
var vars = new ScriptVars
{
["value"] = 13
};
const string script1 = @"
ctx.obj = { number: ctx.value * 2 };
";
const string script2 = @"
return ctx.obj.number + 2;
";
sut.Execute(vars, script1, new ScriptOptions { AsContext = true });
var result = sut.Execute(vars, script2, new ScriptOptions { AsContext = true });
Assert.Equal(JsonValue.Create(28), result);
}
[Fact]
public async Task Should_share_vars_between_execution_for_transform()
{
var vars = new ScriptVars
{
["value"] = 13
};
const string script1 = @"
ctx.obj = { number: ctx.value * 2 };
";
const string script2 = @"
ctx.data.test = { iv: ctx.obj.number + 2 };
replace();
";
sut.Execute(vars, script1, new ScriptOptions { AsContext = true });
var vars2 = new ScriptVars(vars)
{
Data = new ContentData()
};
var result = await sut.TransformAsync(vars2, script2, new ScriptOptions { AsContext = true });
Assert.Equal(JsonValue.Create(28), result["test"]!["iv"]);
}
}
}

30
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs

@ -44,7 +44,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
identity.AddClaim(new Claim(OpenIdClaims.Email, "hello@squidex.io"));
Assert.Equal("hello@squidex.io", GetValue(identity, "user.email"));
const string script = @"
return user.email;
";
Assert.Equal("hello@squidex.io", GetValue(identity, script));
}
[Fact]
@ -54,7 +58,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, "my-picture"));
Assert.Equal(new[] { "my-picture" }, GetValue(identity, "user.claims.picture"));
const string script = @"
return user.claims.picture;
";
Assert.Equal(new[] { "my-picture" }, GetValue(identity, script));
}
[Fact]
@ -64,7 +72,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
identity.AddClaim(new Claim(ClaimTypes.Role, "my-role"));
Assert.Equal(new[] { "my-role" }, GetValue(identity, "user.claims.role"));
const string script = @"
return user.claims.role;
";
Assert.Equal(new[] { "my-role" }, GetValue(identity, script));
}
[Fact]
@ -77,8 +89,16 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
identity.AddClaim(new Claim("claim2", "2a"));
identity.AddClaim(new Claim("claim2", "2b"));
Assert.Equal(new[] { "1a", "1b" }, GetValue(identity, "user.claims.claim1"));
Assert.Equal(new[] { "2a", "2b" }, GetValue(identity, "user.claims.claim2"));
const string script1 = @"
return user.claims.claim1;
";
const string script2 = @"
return user.claims.claim2;
";
Assert.Equal(new[] { "1a", "1b" }, GetValue(identity, script1));
Assert.Equal(new[] { "2a", "2b" }, GetValue(identity, script2));
}
private static void AssetUser(ClaimsIdentity identity, string id, bool isClient, bool isUser)

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static readonly NamedId<DomainId> AppId = NamedId.Of(DomainId.NewGuid(), "my-app");
private static readonly NamedId<DomainId> SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
public static Task ValidateAsync(this IValidator validator, object? value, IList<string> errors,
public static ValueTask ValidateAsync(this IValidator validator, object? value, IList<string> errors,
Schema? schema = null,
ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null,
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
return validator.ValidateAsync(value, context, CreateFormatter(errors));
}
public static Task ValidateAsync(this IField field, object? value, IList<string> errors,
public static ValueTask ValidateAsync(this IField field, object? value, IList<string> errors,
Schema? schema = null,
ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null,

82
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs

@ -62,6 +62,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var (vars, asset) = SetupAssetVars();
var expected = $@"
Text: {asset.FileName} {asset.Id}
";
var script = @"
getAsset(data.assets.iv[0], function (assets) {
var result1 = `Text: ${assets[0].fileName} ${assets[0].id}`;
@ -69,10 +73,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
complete(`${result1}`);
});";
var expected = $@"
Text: {asset.FileName} {asset.Id}
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -83,6 +83,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var (vars, assets) = SetupAssetsVars();
var expected = $@"
Text: {assets[0].FileName} {assets[0].Id}
Text: {assets[1].FileName} {assets[1].Id}
";
var script = @"
getAssets(data.assets.iv, function (assets) {
var result1 = `Text: ${assets[0].fileName} ${assets[0].id}`;
@ -91,11 +96,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
complete(`${result1}\n${result2}`);
});";
var expected = $@"
Text: {assets[0].FileName} {assets[0].Id}
Text: {assets[1].FileName} {assets[1].Id}
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -108,6 +108,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var expected = @"
Text: Hello Asset
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
@ -117,10 +121,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
});
});";
var expected = @"
Text: Hello Asset
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -133,6 +133,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var expected = @"
Text: Hello Asset
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
@ -142,10 +146,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
}, 'utf8');
});";
var expected = @"
Text: Hello Asset
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -158,6 +158,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupText(asset.Id, Encoding.Unicode.GetBytes("Hello Asset"));
var expected = @"
Text: Hello Asset
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
@ -167,10 +171,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
}, 'unicode');
});";
var expected = @"
Text: Hello Asset
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -183,6 +183,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupText(asset.Id, Encoding.ASCII.GetBytes("Hello Asset"));
var expected = @"
Text: Hello Asset
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
@ -192,10 +196,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
}, 'ascii');
});";
var expected = @"
Text: Hello Asset
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -208,6 +208,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var expected = @"
Text: SGVsbG8gQXNzZXQ=
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
@ -217,10 +221,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
}, 'base64');
});";
var expected = @"
Text: SGVsbG8gQXNzZXQ=
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -231,6 +231,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var (vars, _) = SetupAssetVars(1_000_000);
var expected = @"
Text: ErrorTooBig
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
@ -240,10 +244,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
});
});";
var expected = @"
Text: ErrorTooBig
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -270,6 +270,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
["event"] = @event
};
var expected = @"
Text: Hello Asset
";
var script = @"
getAssetText(event, function (text) {
var result = `Text: ${text}`;
@ -277,10 +281,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
complete(result);
});";
var expected = @"
Text: Hello Asset
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -302,6 +302,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
["event"] = @event
};
var expected = @"
Text: ErrorTooBig
";
var script = @"
getAssetText(event, function (text) {
var result = `Text: ${text}`;
@ -309,10 +313,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
complete(result);
});";
var expected = @"
Text: ErrorTooBig
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));

98
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs

@ -6,12 +6,15 @@
// ==========================================================================
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
@ -20,38 +23,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
private readonly NamedId<DomainId> schemaWithScriptId = NamedId.Of(DomainId.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly ScriptContent sut;
public ScriptContentTests()
{
var schemaDef = new Schema(schemaId.Name);
var schemaDefWithScript =
new Schema(schemaWithScriptId.Name)
.SetScripts(new SchemaScripts
{
Query = "my-query"
});
schemaProvider = x =>
{
if (x == schemaId.Id)
{
return Task.FromResult((Mocks.Schema(appId, schemaId, schemaDef), ResolvedComponents.Empty));
}
else if (x == schemaWithScriptId.Id)
{
return Task.FromResult((Mocks.Schema(appId, schemaWithScriptId, schemaDefWithScript), ResolvedComponents.Empty));
}
else
{
throw new DomainObjectNotFoundException(x.ToString());
}
};
sut = new ScriptContent(scriptEngine);
}
@ -60,9 +35,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId));
var content = new ContentEntity { SchemaId = schemaId };
var (provider, schemaId) = CreateSchema(
queryPre: "my-pre-query");
var content = new ContentEntity { Data = new ContentData(), SchemaId = schemaId };
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider, default);
await sut.EnrichAsync(ctx, new[] { content }, provider, default);
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._))
.MustNotHaveHappened();
@ -73,9 +51,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId));
var content = new ContentEntity { SchemaId = schemaWithScriptId };
var (provider, schemaId) = CreateSchema(
query: "my-query");
var content = new ContentEntity { Data = new ContentData(), SchemaId = schemaId };
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider, default);
await sut.EnrichAsync(ctx, new[] { content }, provider, default);
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._))
.MustNotHaveHappened();
@ -88,12 +69,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var oldData = new ContentData();
var content = new ContentEntity { SchemaId = schemaWithScriptId, Data = oldData };
var (provider, schemaId) = CreateSchema(
query: "my-query");
var content = new ContentEntity { Data = oldData, SchemaId = schemaId };
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, "my-query", ScriptOptions(), A<CancellationToken>._))
.Returns(new ContentData());
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider, default);
await sut.EnrichAsync(ctx, new[] { content }, provider, default);
Assert.NotSame(oldData, content.Data);
@ -107,6 +91,52 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
.MustHaveHappened();
}
[Fact]
public async Task Should_make_test_with_pre_query_script()
{
var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId));
var (provider, id) = CreateSchema(
query: @"
ctx.data.test = { iv: ctx.custom };
replace()",
queryPre: "ctx.custom = 123;");
var content = new ContentEntity { Data = new ContentData(), SchemaId = id };
var realScriptEngine =
new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())),
Options.Create(new JintScriptOptions
{
TimeoutScript = TimeSpan.FromSeconds(20),
TimeoutExecution = TimeSpan.FromSeconds(100)
}));
var sut2 = new ScriptContent(realScriptEngine);
await sut2.EnrichAsync(ctx, new[] { content }, provider, default);
Assert.Equal(JsonValue.Create(123), content.Data["test"]!["iv"]);
}
private (ProvideSchema, NamedId<DomainId>) CreateSchema(string? query = null, string? queryPre = null)
{
var id = NamedId.Of(DomainId.NewGuid(), "my-schema");
return (_ =>
{
var schemaDef =
new Schema(id.Name)
.SetScripts(new SchemaScripts
{
Query = query,
QueryPre = queryPre
});
return Task.FromResult((Mocks.Schema(appId, id, schemaDef), ResolvedComponents.Empty));
}, id);
}
private static ScriptOptions ScriptOptions()
{
return A<ScriptOptions>.That.Matches(x => x.AsContext);

18
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs

@ -78,6 +78,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
["user"] = user
};
var expected = @"
Text: Hello 1 World 1
";
var script = @"
getReference(data.references.iv[0], function (references) {
var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
@ -85,10 +89,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
complete(`${result1}`);
})";
var expected = @"
Text: Hello 1 World 1
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
@ -122,6 +122,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
["user"] = user
};
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
var script = @"
getReferences(data.references.iv, function (references) {
var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
@ -130,11 +135,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
complete(`${result1}\n${result2}`);
})";
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));

69
backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs

@ -114,75 +114,6 @@ namespace TestSuite.ApiTests
Assert.False(app_2._links.ContainsKey("image"));
}
[Fact]
public async Task Should_manage_roles()
{
// Use role name with hash to test previous bug.
var roleName = $"{Guid.NewGuid()}/1";
var roleClient = Guid.NewGuid().ToString();
var roleContributor1 = "hello@squidex.io";
// STEP 1: Add role.
var createRequest = new AddRoleDto { Name = roleName };
var roles_1 = await _.Apps.PostRoleAsync(_.AppName, createRequest);
var role_1 = roles_1.Items.Find(x => x.Name == roleName);
// Should return role with correct name.
Assert.Empty(role_1.Permissions);
// STEP 2: Update role.
var updateRequest = new UpdateRoleDto { Permissions = new List<string> { "a", "b" } };
var roles_2 = await _.Apps.PutRoleAsync(_.AppName, roleName, updateRequest);
var role_2 = roles_2.Items.Find(x => x.Name == roleName);
// Should return role with correct name.
Assert.Equal(updateRequest.Permissions, role_2.Permissions);
// STEP 3: Assign client and contributor.
await _.Apps.PostClientAsync(_.AppName, new CreateClientDto { Id = roleClient });
// Add client to role.
await _.Apps.PutClientAsync(_.AppName, roleClient, new UpdateClientDto { Role = roleName });
// Add contributor to role.
await _.Apps.PostContributorAsync(_.AppName, new AssignContributorDto { ContributorId = roleContributor1, Role = roleName, Invite = true });
var roles_3 = await _.Apps.GetRolesAsync(_.AppName);
var role_3 = roles_3.Items.Find(x => x.Name == roleName);
// Should return role with correct number of users and clients.
Assert.Equal(1, role_3.NumClients);
Assert.Equal(1, role_3.NumContributors);
// STEP 4: Try to delete role.
var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => _.Apps.DeleteRoleAsync(_.AppName, roleName));
Assert.Equal(400, ex.StatusCode);
// STEP 5: Remove after client and contributor removed.
var fallbackRole = "Developer";
// Remove client from role.
await _.Apps.PutClientAsync(_.AppName, roleClient, new UpdateClientDto { Role = fallbackRole });
// Remove contributor from role.
await _.Apps.PostContributorAsync(_.AppName, new AssignContributorDto { ContributorId = roleContributor1, Role = fallbackRole });
await _.Apps.DeleteRoleAsync(_.AppName, roleName);
var roles_4 = await _.Apps.GetRolesAsync(_.AppName);
var role_4 = roles_4.Items.Find(x => x.Name == roleName);
// Should not return deleted role.
Assert.Null(role_4);
}
[Fact]
public async Task Should_get_settings()
{

13
backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
using TestSuite.Fixtures;
using TestSuite.Model;
using Xunit;
@ -35,7 +36,7 @@ namespace TestSuite.ApiTests
// STEP 2: Create a content for this schema.
var data = new TestEntityData { Number1 = 12, String = "hello" };
var data = new TestEntityData { Number = 12, String = "hello" };
var content_1 = await contents.CreateAsync(data);
@ -47,7 +48,10 @@ namespace TestSuite.ApiTests
// STEP 4: Make any update.
var content_2 = await contents.ChangeStatusAsync(content_1.Id, "Published");
var content_2 = await contents.ChangeStatusAsync(content_1.Id, new ChangeStatus
{
Status = "Published"
});
// Should not return deleted field.
Assert.Null(content_2.Data.String);
@ -81,7 +85,10 @@ namespace TestSuite.ApiTests
// STEP 4: Make any update.
var contentB_2 = await contents.ChangeStatusAsync(contentB_1.Id, "Published");
var contentB_2 = await contents.ChangeStatusAsync(contentB_1.Id, new ChangeStatus
{
Status = "Published"
});
// Should not return deleted field.
Assert.Empty(contentB_2.Data.References);

97
backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs

@ -46,7 +46,7 @@ namespace TestSuite.ApiTests
{
var items = await _.Contents.GetAsync(new ContentQuery
{
OrderBy = "data/number1/iv asc"
OrderBy = "data/number/iv asc"
});
var itemsById = await _.Contents.GetAsync(new HashSet<string>(items.Items.Take(3).Select(x => x.Id)));
@ -66,7 +66,7 @@ namespace TestSuite.ApiTests
{
var items = await _.Contents.GetAsync(new ContentQuery
{
OrderBy = "data/number1/iv asc"
OrderBy = "data/number/iv asc"
});
AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
@ -83,7 +83,7 @@ namespace TestSuite.ApiTests
{
new
{
path = "data.number1.iv", order = "ascending"
path = "data.number.iv", order = "ascending"
}
}
}
@ -97,7 +97,7 @@ namespace TestSuite.ApiTests
{
var items = await _.Contents.GetAsync(new ContentQuery
{
Skip = 5, OrderBy = "data/number1/iv asc"
Skip = 5, OrderBy = "data/number/iv asc"
});
AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 });
@ -114,7 +114,7 @@ namespace TestSuite.ApiTests
{
new
{
path = "data.number1.iv", order = "ascending"
path = "data.number.iv", order = "ascending"
}
},
skip = 5
@ -129,7 +129,7 @@ namespace TestSuite.ApiTests
{
var items = await _.Contents.GetAsync(new ContentQuery
{
Skip = 2, Top = 5, OrderBy = "data/number1/iv asc"
Skip = 2, Top = 5, OrderBy = "data/number/iv asc"
});
AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 });
@ -146,7 +146,7 @@ namespace TestSuite.ApiTests
{
new
{
path = "data.number1.iv", order = "ascending"
path = "data.number.iv", order = "ascending"
}
},
skip = 2, top = 5
@ -161,7 +161,7 @@ namespace TestSuite.ApiTests
{
var items = await _.Contents.GetAsync(new ContentQuery
{
Filter = "data/number1/iv gt 3 and data/number1/iv lt 7", OrderBy = "data/number1/iv asc"
Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc"
});
AssertItems(items, 3, new[] { 4, 5, 6 });
@ -178,7 +178,7 @@ namespace TestSuite.ApiTests
{
new
{
path = "data.number1.iv", order = "ascending"
path = "data.number.iv", order = "ascending"
}
},
filter = new
@ -187,13 +187,13 @@ namespace TestSuite.ApiTests
{
new
{
path = "data.number1.iv",
path = "data.number.iv",
op = "gt",
value = 3
},
new
{
path = "data.number1.iv",
path = "data.number.iv",
op = "lt",
value = 7
}
@ -418,19 +418,13 @@ namespace TestSuite.ApiTests
query = @"
mutation {
createMyReadsContent(data: {
number1: {
number: {
iv: 555
},
number2: {
iv: 556
}
}) {
id,
data {
number1 {
iv
},
number2 {
number {
iv
}
}
@ -440,11 +434,9 @@ namespace TestSuite.ApiTests
var result = await _.Contents.GraphQlAsync<JObject>(query);
var value1 = result["createMyReadsContent"]["data"]["number1"]["iv"].Value<int>();
var value2 = result["createMyReadsContent"]["data"]["number2"]["iv"].Value<int>();
var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>();
Assert.Equal(555, value1);
Assert.Equal(556, value2);
Assert.Equal(555, value);
}
[Fact]
@ -457,10 +449,7 @@ namespace TestSuite.ApiTests
createMyReadsContent(data: $data) {
id,
data {
number1 {
iv
}
number2 {
number {
iv
}
}
@ -470,13 +459,9 @@ namespace TestSuite.ApiTests
{
data = new
{
number1 = new
number = new
{
iv = 998
},
number2 = new
{
iv = 999
}
}
}
@ -484,11 +469,9 @@ namespace TestSuite.ApiTests
var result = await _.Contents.GraphQlAsync<JObject>(query);
var value1 = result["createMyReadsContent"]["data"]["number1"]["iv"].Value<int>();
var value2 = result["createMyReadsContent"]["data"]["number2"]["iv"].Value<int>();
var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>();
Assert.Equal(998, value1);
Assert.Equal(999, value2);
Assert.Equal(998, value);
}
[Fact]
@ -498,10 +481,10 @@ namespace TestSuite.ApiTests
{
query = @"
query ContentsQuery($filter: String!) {
queryMyReadsContents(filter: $filter, orderby: ""data/number1/iv asc"") {
queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") {
id,
data {
number1 {
number {
iv
}
}
@ -509,7 +492,7 @@ namespace TestSuite.ApiTests
}",
variables = new
{
filter = @"data/number1/iv gt 3 and data/number1/iv lt 7"
filter = @"data/number/iv gt 3 and data/number/iv lt 7"
}
};
@ -517,10 +500,10 @@ namespace TestSuite.ApiTests
{
query = @"
query ContentsQuery($filter: String!) {
queryMyReadsContents(filter: $filter, orderby: ""data/number1/iv asc"") {
queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") {
id,
data {
number1 {
number {
iv
}
}
@ -528,7 +511,7 @@ namespace TestSuite.ApiTests
}",
variables = new
{
filter = @"data/number1/iv gt 4 and data/number1/iv lt 7"
filter = @"data/number/iv gt 4 and data/number/iv lt 7"
}
};
@ -537,8 +520,8 @@ namespace TestSuite.ApiTests
var items1 = results.ElementAt(0).Data.Items;
var items2 = results.ElementAt(1).Data.Items;
Assert.Equal(items1.Select(x => x.Data.Number1).ToArray(), new[] { 4, 5, 6 });
Assert.Equal(items2.Select(x => x.Data.Number1).ToArray(), new[] { 5, 6 });
Assert.Equal(items1.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 });
Assert.Equal(items2.Select(x => x.Data.Number).ToArray(), new[] { 5, 6 });
}
[Fact]
@ -548,10 +531,10 @@ namespace TestSuite.ApiTests
{
query = @"
query ContentsQuery($filter: String!) {
queryMyReadsContents(filter: $filter, orderby: ""data/number1/iv asc"") {
queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") {
id,
data {
number1 {
number {
iv
}
}
@ -559,7 +542,7 @@ namespace TestSuite.ApiTests
}",
variables = new
{
filter = @"data/number1/iv gt 3 and data/number1/iv lt 7"
filter = @"data/number/iv gt 3 and data/number/iv lt 7"
}
};
@ -567,7 +550,7 @@ namespace TestSuite.ApiTests
var items = result.Items;
Assert.Equal(items.Select(x => x.Data.Number1).ToArray(), new[] { 4, 5, 6 });
Assert.Equal(items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 });
}
[Fact]
@ -577,10 +560,10 @@ namespace TestSuite.ApiTests
{
query = @"
query ContentsQuery($filter: String!) {
queryMyReadsContents(filter: $filter, orderby: ""data/number1/iv asc"") {
queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") {
id,
data {
number1 {
number {
iv
}
}
@ -588,7 +571,7 @@ namespace TestSuite.ApiTests
}",
variables = new
{
filter = @"data/number1/iv gt 3 and data/number1/iv lt 7"
filter = @"data/number/iv gt 3 and data/number/iv lt 7"
}
};
@ -596,7 +579,7 @@ namespace TestSuite.ApiTests
var items = result.Items;
Assert.Equal(items.Select(x => x.Data.Number1).ToArray(), new[] { 4, 5, 6 });
Assert.Equal(items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 });
}
[Fact]
@ -606,10 +589,10 @@ namespace TestSuite.ApiTests
{
query = @"
{
queryMyReadsContents(filter: ""data/number1/iv gt 3 and data/number1/iv lt 7"", orderby: ""data/number1/iv asc"") {
queryMyReadsContents(filter: ""data/number/iv gt 3 and data/number/iv lt 7"", orderby: ""data/number/iv asc"") {
id,
data {
number1 {
number {
iv
}
}
@ -621,7 +604,7 @@ namespace TestSuite.ApiTests
var items = result["queryMyReadsContents"];
Assert.Equal(items.Select(x => x["data"]["number1"]["iv"].Value<int>()).ToArray(), new[] { 4, 5, 6 });
Assert.Equal(items.Select(x => x["data"]["number"]["iv"].Value<int>()).ToArray(), new[] { 4, 5, 6 });
}
private sealed class QueryResult
@ -640,13 +623,13 @@ namespace TestSuite.ApiTests
private sealed class QueryItemData
{
[JsonConverter(typeof(InvariantConverter))]
public int Number1 { get; set; }
public int Number { get; set; }
}
private static void AssertItems(ContentsResult<TestEntity, TestEntityData> entities, int total, int[] expected)
{
Assert.Equal(total, entities.Total);
Assert.Equal(expected, entities.Items.Select(x => x.Data.Number1).ToArray());
Assert.Equal(expected, entities.Items.Select(x => x.Data.Number).ToArray());
}
}
}

30
backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs

@ -36,7 +36,7 @@ namespace TestSuite.ApiTests
// STEP 2: Create a content with a reference.
var dataB = new TestEntityWithReferencesData { References = new[] { contentA_1.Id } };
var contentB_1 = await _.Contents.CreateAsync(dataB, true);
var contentB_1 = await _.Contents.CreateAsync(dataB, ContentCreateOptions.AsPublish);
// STEP 3: Query new item
@ -46,7 +46,10 @@ namespace TestSuite.ApiTests
// STEP 4: Publish reference
await _.Contents.ChangeStatusAsync(contentA_1.Id, "Published");
await _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus
{
Status = "Published"
});
// STEP 5: Query new item again
@ -61,21 +64,24 @@ namespace TestSuite.ApiTests
// STEP 1: Create a referenced content.
var dataA = new TestEntityWithReferencesData();
var contentA_1 = await _.Contents.CreateAsync(dataA, true);
var contentA_1 = await _.Contents.CreateAsync(dataA, ContentCreateOptions.AsPublish);
// STEP 2: Create a content with a reference.
var dataB = new TestEntityWithReferencesData { References = new[] { contentA_1.Id } };
await _.Contents.CreateAsync(dataB, true);
await _.Contents.CreateAsync(dataB, ContentCreateOptions.AsPublish);
// STEP 3: Try to delete with referrer check.
await Assert.ThrowsAnyAsync<SquidexException>(() => _.Contents.DeleteAsync(contentA_1.Id, checkReferrers: true));
await Assert.ThrowsAnyAsync<SquidexException>(() => _.Contents.DeleteAsync(contentA_1.Id, new ContentDeleteOptions
{
CheckReferrers = true
}));
// STEP 4: Delete without referrer check
await _.Contents.DeleteAsync(contentA_1.Id, checkReferrers: false);
await _.Contents.DeleteAsync(contentA_1.Id);
}
[Fact]
@ -84,13 +90,13 @@ namespace TestSuite.ApiTests
// STEP 1: Create a published referenced content.
var dataA = new TestEntityWithReferencesData();
var contentA_1 = await _.Contents.CreateAsync(dataA, true);
var contentA_1 = await _.Contents.CreateAsync(dataA, ContentCreateOptions.AsPublish);
// STEP 2: Create a content with a reference.
var dataB = new TestEntityWithReferencesData { References = new[] { contentA_1.Id } };
await _.Contents.CreateAsync(dataB, true);
await _.Contents.CreateAsync(dataB, ContentCreateOptions.AsPublish);
// STEP 3: Try to ThrowsAnyAsync with referrer check.
@ -115,13 +121,13 @@ namespace TestSuite.ApiTests
// STEP 1: Create a referenced content.
var dataA = new TestEntityWithReferencesData();
var contentA_1 = await _.Contents.CreateAsync(dataA, true);
var contentA_1 = await _.Contents.CreateAsync(dataA, ContentCreateOptions.AsPublish);
// STEP 2: Create a content with a reference.
var dataB = new TestEntityWithReferencesData { References = new[] { contentA_1.Id } };
await _.Contents.CreateAsync(dataB, true);
await _.Contents.CreateAsync(dataB, ContentCreateOptions.AsPublish);
// STEP 3: Try to delete with referrer check.
@ -166,13 +172,13 @@ namespace TestSuite.ApiTests
// STEP 1: Create a published referenced content.
var dataA = new TestEntityWithReferencesData();
var contentA_1 = await _.Contents.CreateAsync(dataA, true);
var contentA_1 = await _.Contents.CreateAsync(dataA, ContentCreateOptions.AsPublish);
// STEP 2: Create a published content with a reference.
var dataB = new TestEntityWithReferencesData { References = new[] { contentA_1.Id } };
await _.Contents.CreateAsync(dataB, true);
await _.Contents.CreateAsync(dataB, ContentCreateOptions.AsPublish);
// STEP 3: Try to delete with referrer check.

164
backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
using TestSuite.Model;
@ -25,36 +26,177 @@ namespace TestSuite.ApiTests
}
[Fact]
public async Task Should_use_creating_and_query_tests()
public async Task Should_create_content_with_scripting()
{
var schemaName = $"schema-{Guid.NewGuid()}";
var scripts = new SchemaScriptsDto
{
Create = @$"
ctx.data.{TestEntityData.NumberField}.iv *= 2;
replace()"
};
// STEP 1: Create a schema.
await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName);
await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts);
// STEP 2: Create content
var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName);
var content = await contents.CreateAsync(new TestEntityData { Number = 13 });
Assert.Equal(26, content.Data.Number);
}
// STEP 2: Set scripts
await _.Schemas.PutScriptsAsync(_.AppName, schemaName, new SchemaScriptsDto
[Fact]
public async Task Should_query_content_with_scripting()
{
var schemaName = $"schema-{Guid.NewGuid()}";
var scripts = new SchemaScriptsDto
{
Query = @$"
ctx.data.{TestEntityData.StringField}.iv = ctx.data.{TestEntityData.StringField}.iv + '_Updated'
ctx.data.{TestEntityData.NumberField}.iv *= 2;
replace()",
};
// STEP 1: Create a schema.
await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts);
// STEP 2: Create content
var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName);
var content = await contents.CreateAsync(new TestEntityData { Number = 13 }, ContentCreateOptions.AsPublish);
Assert.Equal(26, content.Data.Number);
}
[Fact]
public async Task Should_query_content_with_scripting_and_pre_query()
{
var schemaName = $"schema-{Guid.NewGuid()}";
var scripts = new SchemaScriptsDto
{
QueryPre = @$"
ctx.test = 17",
Query = @$"
ctx.data.{TestEntityData.NumberField}.iv = ctx.test + 2;
replace()",
};
// STEP 1: Create a schema.
await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts);
// STEP 2: Create content
var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName);
var content = await contents.CreateAsync(new TestEntityData { Number = 99 }, ContentCreateOptions.AsPublish);
Assert.Equal(19, content.Data.Number);
}
[Fact]
public async Task Should_create_bulk_content_with_scripting()
{
var schemaName = $"schema-{Guid.NewGuid()}";
// STEP 1: Create a schema.
var scripts = new SchemaScriptsDto
{
Create = @$"
ctx.data.{TestEntityData.Number1Field}.iv *= 2;
ctx.data.{TestEntityData.NumberField}.iv = incrementCounter('${schemaName}');
replace()"
};
await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts);
// STEP 2: Create content with a value that triggers the schema.
var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName);
var results = await contents.BulkUpdateAsync(new BulkUpdate
{
DoNotScript = false,
DoNotValidate = false,
Jobs = new List<BulkUpdateJob>
{
new BulkUpdateJob
{
Type = BulkUpdateType.Upsert,
Data = new
{
number = new
{
iv = 99
}
}
}
},
Publish = true
});
Assert.Single(results);
Assert.Null(results[0].Error);
// STEP 2: Query content.
var content = await contents.GetAsync(results[0].ContentId);
// STEP 3: Create content
Assert.True(content.Data.Number > 0);
}
[Fact]
public async Task Should_create_bulk_content_with_scripting_but_disabled()
{
var schemaName = $"schema-{Guid.NewGuid()}";
// STEP 1: Create a schema.
var scripts = new SchemaScriptsDto
{
Create = @$"
ctx.data.{TestEntityData.NumberField}.iv = incrementCounter('${schemaName}');
replace()"
};
await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts);
// STEP 1: Create content with a value that triggers the schema.
var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName);
var data = new TestEntityData { Number1 = 13, String = "Hello" };
var results = await contents.BulkUpdateAsync(new BulkUpdate
{
DoNotScript = true,
DoNotValidate = false,
Jobs = new List<BulkUpdateJob>
{
new BulkUpdateJob
{
Type = BulkUpdateType.Upsert,
Data = new
{
number = new
{
iv = 99
}
}
}
},
Publish = true
});
Assert.Single(results);
Assert.Null(results[0].Error);
var content = await contents.CreateAsync(data);
// STEP 2: Query content.
var content = await contents.GetAsync(results[0].ContentId);
Assert.Equal(26, content.Data.Number1);
Assert.Equal("Hello_Updated", content.Data.String);
Assert.Equal(99, content.Data.Number);
}
}
}

222
backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -34,11 +34,14 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create the item unpublished.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 });
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
// STEP 2: Publish the item.
await _.Contents.ChangeStatusAsync(content.Id, Status.Published);
await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus
{
Status = "Published"
});
// STEP 3: Retrieve the item.
@ -60,11 +63,14 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create the item published.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 }, true);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, ContentCreateOptions.AsPublish);
// STEP 2: Archive the item.
await _.Contents.ChangeStatusAsync(content.Id, Status.Archived);
await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus
{
Status = "Archived"
});
// STEP 3. Get a 404 for the item because it is not published anymore.
@ -86,12 +92,18 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create the item unpublished.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 });
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
// STEP 2: Change the status to publiushed and then to draft.
await _.Contents.ChangeStatusAsync(content.Id, Status.Published);
await _.Contents.ChangeStatusAsync(content.Id, Status.Draft);
await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus
{
Status = "Published"
});
await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus
{
Status = "Draft"
});
// STEP 3. Get a 404 for the item because it is not published anymore.
@ -115,7 +127,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a content item with a text that caused a bug before.
content = await _.Contents.CreateAsync(new TestEntityData { String = text }, true);
content = await _.Contents.CreateAsync(new TestEntityData { String = text }, ContentCreateOptions.AsPublish);
// STEP 2: Get the item and ensure that the text is the same.
@ -145,7 +157,7 @@ namespace TestSuite.ApiTests
{
["en"] = null
}
}, true);
}, ContentCreateOptions.AsPublish);
// STEP 2: Get the item and ensure that the text is the same.
@ -172,7 +184,7 @@ namespace TestSuite.ApiTests
content = await _.Contents.CreateAsync(new TestEntityData
{
Localized = new Dictionary<string, string>()
}, true);
}, ContentCreateOptions.AsPublish);
// STEP 2: Get the item and ensure that the text is the same.
@ -189,115 +201,6 @@ namespace TestSuite.ApiTests
}
}
[Fact]
public async Task Should_create_content_with_scripting()
{
TestEntity content = null;
try
{
// STEP 1: Create a content item with a value that triggers the schema.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = -99 }, true);
Assert.True(content.Data.Number1 > 0);
}
finally
{
if (content != null)
{
await _.Contents.DeleteAsync(content.Id);
}
}
}
[Fact]
public async Task Should_create_bulk_content_with_scripting()
{
TestEntity content = null;
try
{
// STEP 1: Create content with a value that triggers the schema.
var results = await _.Contents.BulkUpdateAsync(new BulkUpdate
{
DoNotScript = false,
Jobs = new List<BulkUpdateJob>
{
new BulkUpdateJob
{
Type = BulkUpdateType.Upsert,
Data = new
{
number1 = new
{
iv = TestEntity.ScriptTrigger
}
}
}
},
Publish = true
});
Assert.Single(results);
Assert.Null(results[0].Error);
// STEP 2: Query content.
content = await _.Contents.GetAsync(results[0].ContentId);
Assert.True(content.Data.Number1 > 0);
}
finally
{
if (content != null)
{
await _.Contents.DeleteAsync(content.Id);
}
}
}
[Fact]
public async Task Should_create_bulk_content_with_scripting_but_disabled()
{
TestEntity content = null;
try
{
// STEP 1: Create content with a value that triggers the schema.
var results = await _.Contents.BulkUpdateAsync(new BulkUpdate
{
Jobs = new List<BulkUpdateJob>
{
new BulkUpdateJob
{
Type = BulkUpdateType.Upsert,
Data = new
{
number1 = new
{
iv = TestEntity.ScriptTrigger
}
}
}
},
Publish = true
});
Assert.Single(results);
Assert.Null(results[0].Error);
// STEP 2: Query content.
content = await _.Contents.GetAsync(results[0].ContentId);
Assert.Equal(-99, content.Data.Number1);
}
finally
{
if (content != null)
{
await _.Contents.DeleteAsync(content.Id);
}
}
}
[Fact]
public async Task Should_create_non_published_content()
{
@ -305,7 +208,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create the item unpublished.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 });
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
// STEP 2. Get a 404 for the item because it is not published.
@ -327,7 +230,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create the item published.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 }, true);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, ContentCreateOptions.AsPublish);
// STEP 2: Get the item.
@ -351,7 +254,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item with a custom id.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 }, id, true);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, new ContentCreateOptions { Id = id, Publish = true });
Assert.Equal(id, content.Id);
}
@ -373,13 +276,13 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item with a custom id.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 }, id, true);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, new ContentCreateOptions { Id = id, Publish = true });
Assert.Equal(id, content.Id);
// STEP 2: Create a new item with a custom id.
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => _.Contents.CreateAsync(new TestEntityData { Number1 = 1 }, id, true));
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => _.Contents.CreateAsync(new TestEntityData { Number = 1 }, new ContentCreateOptions { Id = id, Publish = true }));
Assert.Contains("\"statusCode\":409", ex.Message, StringComparison.Ordinal);
}
@ -401,21 +304,21 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Upsert a new item with a custom id.
content = await _.Contents.UpsertAsync(id, new TestEntityData { Number1 = 1 }, true);
content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 1 }, ContentUpsertOptions.AsPublish);
Assert.Equal(id, content.Id);
// STEP 2: Make an update with the upsert endpoint.
content = await _.Contents.UpsertAsync(id, new TestEntityData { Number1 = 2 });
content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 2 });
Assert.Equal(2, content.Data.Number1);
Assert.Equal(2, content.Data.Number);
// STEP 3: Make an update with the update endpoint.
content = await _.Contents.UpdateAsync(id, new TestEntityData { Number1 = 3 });
content = await _.Contents.UpdateAsync(id, new TestEntityData { Number = 3 });
Assert.Equal(3, content.Data.Number1);
Assert.Equal(3, content.Data.Number);
}
finally
{
@ -433,15 +336,15 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 2 }, true);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, ContentCreateOptions.AsPublish);
// STEP 2: Update the item and ensure that the data has changed.
await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number1 = 2 });
await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = 2 });
var updated = await _.Contents.GetAsync(content.Id);
Assert.Equal(2, content.Data.Number1);
Assert.Equal(2, content.Data.Number);
}
finally
{
@ -459,7 +362,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { String = "initial" }, true);
content = await _.Contents.CreateAsync(new TestEntityData { String = "initial" }, ContentCreateOptions.AsPublish);
// STEP 2: Update the item and ensure that the data has changed.
@ -485,19 +388,19 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { String = "test" }, true);
content = await _.Contents.CreateAsync(new TestEntityData { String = "test" }, ContentCreateOptions.AsPublish);
// STEP 2: Path an item.
await _.Contents.PatchAsync(content.Id, new TestEntityData { Number1 = 1 });
await _.Contents.PatchAsync(content.Id, new TestEntityData { Number = 1 });
// STEP 3: Update the item and ensure that the data has changed.
await _.Contents.PatchAsync(content.Id, new TestEntityData { Number1 = 2 });
await _.Contents.PatchAsync(content.Id, new TestEntityData { Number = 2 });
var updated = await _.Contents.GetAsync(content.Id);
Assert.Equal(2, updated.Data.Number1);
Assert.Equal(2, updated.Data.Number);
// Should not change other value with patch.
Assert.Equal("test", updated.Data.String);
@ -518,7 +421,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { Id = "id1" }, true);
content = await _.Contents.CreateAsync(new TestEntityData { Id = "id1" }, ContentCreateOptions.AsPublish);
// STEP 2: Update the item and ensure that the data has changed.
@ -544,7 +447,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { String = "initial" }, true);
content = await _.Contents.CreateAsync(new TestEntityData { String = "initial" }, ContentCreateOptions.AsPublish);
// STEP 2: Update the item and ensure that the data has changed.
@ -570,7 +473,7 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 }, true);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, ContentCreateOptions.AsPublish);
// STEP 2: Create draft.
@ -578,25 +481,28 @@ namespace TestSuite.ApiTests
// STEP 3: Update the item and ensure that the data has not changed.
await _.Contents.PatchAsync(content.Id, new TestEntityData { Number1 = 2 });
await _.Contents.PatchAsync(content.Id, new TestEntityData { Number = 2 });
var updated_1 = await _.Contents.GetAsync(content.Id);
Assert.Equal(1, updated_1.Data.Number1);
Assert.Equal(1, updated_1.Data.Number);
// STEP 4: Get the unpublished version
var unpublished = await _.Contents.GetAsync(content.Id, QueryContext.Default.Unpublished());
Assert.Equal(2, unpublished.Data.Number1);
Assert.Equal(2, unpublished.Data.Number);
// STEP 5: Publish draft and ensure that it has been updated.
await _.Contents.ChangeStatusAsync(content.Id, "Published");
await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus
{
Status = "Published"
});
var updated_2 = await _.Contents.GetAsync(content.Id);
Assert.Equal(2, updated_2.Data.Number1);
Assert.Equal(2, updated_2.Data.Number);
}
finally
{
@ -613,11 +519,11 @@ namespace TestSuite.ApiTests
public async Task Should_delete_content(bool permanent)
{
// STEP 1: Create a new item.
var content_1 = await _.Contents.CreateAsync(new TestEntityData { Number1 = 2 }, true);
var content_1 = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, ContentCreateOptions.AsPublish);
// STEP 2: Delete the item.
await _.Contents.DeleteAsync(content_1.Id, permanent);
await _.Contents.DeleteAsync(content_1.Id, new ContentDeleteOptions { Permanent = permanent });
// STEP 3: Retrieve all items and ensure that the deleted item does not exist.
@ -641,15 +547,15 @@ namespace TestSuite.ApiTests
public async Task Should_recreate_deleted_content(bool permanent)
{
// STEP 1: Create a new item.
var content_1 = await _.Contents.CreateAsync(new TestEntityData { Number1 = 2 }, true);
var content_1 = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, ContentCreateOptions.AsPublish);
// STEP 2: Delete the item.
await _.Contents.DeleteAsync(content_1.Id, permanent);
await _.Contents.DeleteAsync(content_1.Id, new ContentDeleteOptions { Permanent = permanent });
// STEP 3: Recreate the item with the same id.
var content_2 = await _.Contents.CreateAsync(new TestEntityData { Number1 = 2 }, content_1.Id, true);
var content_2 = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, new ContentCreateOptions { Id = content_1.Id, Publish = true });
Assert.Equal(Status.Published, content_2.Status);
@ -666,15 +572,15 @@ namespace TestSuite.ApiTests
public async Task Should_recreate_deleted_content_with_upsert(bool permanent)
{
// STEP 1: Create a new item.
var content_1 = await _.Contents.CreateAsync(new TestEntityData { Number1 = 2 }, true);
var content_1 = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, ContentCreateOptions.AsPublish);
// STEP 2: Delete the item.
await _.Contents.DeleteAsync(content_1.Id, permanent);
await _.Contents.DeleteAsync(content_1.Id, new ContentDeleteOptions { Permanent = permanent });
// STEP 3: Recreate the item with the same id.
var content_2 = await _.Contents.UpsertAsync(content_1.Id, new TestEntityData { Number1 = 2 }, true);
var content_2 = await _.Contents.UpsertAsync(content_1.Id, new TestEntityData { Number = 2 }, ContentUpsertOptions.AsPublish);
Assert.Equal(Status.Published, content_2.Status);
@ -736,29 +642,29 @@ namespace TestSuite.ApiTests
try
{
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { Number1 = 1 }, true);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, ContentCreateOptions.AsPublish);
// STEP 2: Update content.
content = await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number1 = 2 });
content = await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = 2 });
// STEP 3: Get current version.
var content_latest = await _.Contents.GetAsync(content.Id);
Assert.Equal(2, content_latest.Data.Number1);
Assert.Equal(2, content_latest.Data.Number);
// STEP 4: Get current version.
var data_2 = await _.Contents.GetDataAsync(content.Id, content.Version);
Assert.Equal(2, data_2.Number1);
Assert.Equal(2, data_2.Number);
// STEP 4: Get previous version
var data_1 = await _.Contents.GetDataAsync(content.Id, content.Version - 1);
Assert.Equal(1, data_1.Number1);
Assert.Equal(1, data_1.Number);
}
finally
{

6
backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

@ -192,7 +192,7 @@ namespace TestSuite.ApiTests
var citiesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("cities");
var city = await citiesClient.CreateAsync(cityData, true);
var city = await citiesClient.CreateAsync(cityData, ContentCreateOptions.AsPublish);
// STEP 2: Create city
@ -210,7 +210,7 @@ namespace TestSuite.ApiTests
var statesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("states");
var state = await statesClient.CreateAsync(stateData, true);
var state = await statesClient.CreateAsync(stateData, ContentCreateOptions.AsPublish);
// STEP 3: Create country
@ -228,7 +228,7 @@ namespace TestSuite.ApiTests
var countriesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("countries");
await countriesClient.CreateAsync(countryData, true);
await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish);
}
}
}

2
backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -14,7 +14,7 @@
<None Remove="Assets\SampleImage_WEBP_350kb - Copy.webp" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.679">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

10
backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs

@ -61,7 +61,7 @@ namespace TestSuite.LoadTests
{
await Run.Parallel(numUsers, numIterationsPerUser, async () =>
{
await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number1/iv asc" });
await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number/iv asc" });
});
}
@ -71,7 +71,7 @@ namespace TestSuite.LoadTests
{
await Run.Parallel(numUsers, numIterationsPerUser, async () =>
{
await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/number1/iv asc" });
await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/number/iv asc" });
});
}
@ -81,7 +81,7 @@ namespace TestSuite.LoadTests
{
await Run.Parallel(numUsers, numIterationsPerUser, async () =>
{
await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number1/iv asc" });
await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" });
});
}
@ -91,7 +91,7 @@ namespace TestSuite.LoadTests
{
await Run.Parallel(numUsers, numIterationsPerUser, async () =>
{
await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number1/iv desc" });
await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv desc" });
});
}
@ -101,7 +101,7 @@ namespace TestSuite.LoadTests
{
await Run.Parallel(numUsers, numIterationsPerUser, async () =>
{
await _.Contents.GetAsync(new ContentQuery { Filter = "data/number1/iv gt 3 and data/number1/iv lt 7", OrderBy = "data/number1/iv asc" });
await _.Contents.GetAsync(new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" });
});
}
}

2
backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj

@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.679">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

3
backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
using TestSuite.Model;
using Xunit;
@ -61,7 +62,7 @@ namespace TestSuite.LoadTests
await Run.Parallel(numUsers, numIterationsPerUser, async () =>
{
await _.Contents.CreateAsync(new TestEntityData { Number1 = random.Next() }, true);
await _.Contents.CreateAsync(new TestEntityData { Number = random.Next() }, ContentCreateOptions.AsPublish);
});
}
}

8
backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture1to10.cs

@ -7,6 +7,7 @@
using System.Globalization;
using Newtonsoft.Json.Linq;
using Squidex.ClientLibrary;
using TestSuite.Model;
namespace TestSuite.Fixtures
@ -34,15 +35,14 @@ namespace TestSuite.Fixtures
var data = new TestEntityData
{
String = text,
Number1 = i,
Number2 = i,
Json = JObject.FromObject(new
{
nested1 = new
{
nested2 = i
}
})
}),
Number = i,
};
if (i % 2 == 0)
@ -54,7 +54,7 @@ namespace TestSuite.Fixtures
data.Geo = new { longitude = i, latitude = i };
}
await Contents.CreateAsync(data, true);
await Contents.CreateAsync(data, ContentCreateOptions.AsPublish);
}
}).Wait();
}

32
backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs

@ -16,7 +16,7 @@ namespace TestSuite.Model
{
public const int ScriptTrigger = -99;
public static async Task<SchemaDto> CreateSchemaAsync(ISchemasClient schemas, string appName, string name)
public static async Task<SchemaDto> CreateSchemaAsync(ISchemasClient schemas, string appName, string name, SchemaScriptsDto scripts = null)
{
var schema = await schemas.PostSchemaAsync(appName, new CreateSchemaDto
{
@ -25,21 +25,13 @@ namespace TestSuite.Model
{
new UpsertSchemaFieldDto
{
Name = TestEntityData.Number1Field,
Name = TestEntityData.NumberField,
Properties = new NumberFieldPropertiesDto
{
IsRequired = true
}
},
new UpsertSchemaFieldDto
{
Name = TestEntityData.Number2Field,
Properties = new NumberFieldPropertiesDto
{
IsRequired = false
}
},
new UpsertSchemaFieldDto
{
Name = TestEntityData.StringField,
Properties = new StringFieldPropertiesDto
@ -79,16 +71,9 @@ namespace TestSuite.Model
{
IsRequired = false
}
},
},
Scripts = new SchemaScriptsDto
{
Create = $@"
if (ctx.data.{TestEntityData.Number1Field}.iv === {ScriptTrigger}) {{
ctx.data.{TestEntityData.Number1Field}.iv = incrementCounter('my');
replace();
}}"
}
},
Scripts = scripts,
IsPublished = true
});
@ -102,9 +87,7 @@ namespace TestSuite.Model
public static readonly string StringField = nameof(String).ToLowerInvariant();
public static readonly string Number1Field = nameof(Number1).ToLowerInvariant();
public static readonly string Number2Field = nameof(Number2).ToLowerInvariant();
public static readonly string NumberField = nameof(Number).ToLowerInvariant();
public static readonly string JsonField = nameof(Json).ToLowerInvariant();
@ -115,10 +98,7 @@ namespace TestSuite.Model
public Dictionary<string, string> Localized { get; set; }
[JsonConverter(typeof(InvariantConverter))]
public int Number1 { get; set; }
[JsonConverter(typeof(InvariantConverter))]
public int Number2 { get; set; }
public int Number { get; set; }
[JsonConverter(typeof(InvariantConverter))]
public string Id { get; set; }

6
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -10,8 +10,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Lazy.Fody" Version="1.10.0" PrivateAssets="all" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.679">
<PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -21,7 +21,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="7.7.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup>

1
frontend/src/app/features/schemas/declarations.ts

@ -41,6 +41,7 @@ export * from './pages/schema/preview/schema-preview-urls-form.component';
export * from './pages/schema/rules/schema-field-rules-form.component';
export * from './pages/schema/schema-page.component';
export * from './pages/schema/scripts/schema-scripts-form.component';
export * from './pages/schema/scripts/schema-scripts.pipes';
export * from './pages/schema/ui/field-list.component';
export * from './pages/schema/ui/schema-ui-form.component';
export * from './pages/schemas/schema-form.component';

3
frontend/src/app/features/schemas/module.ts

@ -8,7 +8,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HelpComponent, LoadSchemasGuard, SchemaMustExistGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ArrayValidationComponent, AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, ComponentsUIComponent, ComponentsValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, FieldComponent, FieldFormCommonComponent, FieldFormComponent, FieldFormUIComponent, FieldFormValidationComponent, FieldListComponent, FieldWizardComponent, GeolocationUIComponent, GeolocationValidationComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, NumberValidationComponent, ReferencesUIComponent, ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, SchemaFieldRulesFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, SchemaPreviewUrlsFormComponent, SchemaScriptsFormComponent, SchemasPageComponent, SchemaUIFormComponent, StringUIComponent, StringValidationComponent, TagsUIComponent, TagsValidationComponent } from './declarations';
import { ArrayValidationComponent, AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, ComponentsUIComponent, ComponentsValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, FieldComponent, FieldFormCommonComponent, FieldFormComponent, FieldFormUIComponent, FieldFormValidationComponent, FieldListComponent, FieldWizardComponent, GeolocationUIComponent, GeolocationValidationComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, NumberValidationComponent, ReferencesUIComponent, ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, SchemaFieldRulesFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, SchemaPreviewUrlsFormComponent, SchemaScriptNamePipe, SchemaScriptsFormComponent, SchemasPageComponent, SchemaUIFormComponent, StringUIComponent, StringValidationComponent, TagsUIComponent, TagsValidationComponent } from './declarations';
import { ComponentUIComponent } from './pages/schema/fields/types/component-ui.component';
import { ComponentValidationComponent } from './pages/schema/fields/types/component-validation.component';
@ -80,6 +80,7 @@ const routes: Routes = [
SchemaPageComponent,
SchemaPreviewUrlsFormComponent,
SchemaScriptsFormComponent,
SchemaScriptNamePipe,
SchemasPageComponent,
SchemaUIFormComponent,
StringUIComponent,

2
frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.html

@ -3,7 +3,7 @@
<ul class="nav nav-tabs2">
<li class="nav-item" *ngFor="let script of editForm.form.controls | sqxKeys">
<a class="nav-link" [class.active]="schemaScript === script" (click)="selectField(script)">
{{script | titlecase}}
{{script | sqxSchemaScriptName}}
</a>
</li>
</ul>

24
frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts.pipes.spec.ts

@ -0,0 +1,24 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { SchemaScriptNamePipe } from './schema-scripts.pipes';
describe('SchemaScriptsPipe', () => {
const pipe = new SchemaScriptNamePipe();
it('should return titlecase for schema name', () => {
const actual = pipe.transform('create');
expect(actual).toEqual('Create');
});
it('should return custom name for queryPre', () => {
const actual = pipe.transform('queryPre');
expect(actual).toEqual('Prepare Query');
});
});

22
frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts.pipes.ts

@ -0,0 +1,22 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sqxSchemaScriptName',
pure: true,
})
export class SchemaScriptNamePipe implements PipeTransform {
public transform(value: string) {
if (value === 'queryPre') {
return 'Prepare Query';
} else {
return value.substring(0, 1).toUpperCase() + value.substring(1);
}
}
}

3
frontend/src/app/shared/state/schemas.forms.ts

@ -181,6 +181,9 @@ export class EditSchemaScriptsForm extends Form<ExtendedFormGroup, {}, object> {
query: new FormControl('',
Validators.nullValidator,
),
queryPre: new FormControl('',
Validators.nullValidator,
),
create: new FormControl('',
Validators.nullValidator,
),

Loading…
Cancel
Save