Browse Source

Field selector (#1002)

* Temp

* Fix backup and warnings.

* Fix tests

* Another test fix.

* Fix resolve url.

* Fix tests and add headers.

* Another test fix.

* Add some comments.
pull/1005/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
835031da6d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      backend/.editorconfig
  2. 12
      backend/i18n/frontend_en.json
  3. 12
      backend/i18n/frontend_fr.json
  4. 12
      backend/i18n/frontend_it.json
  5. 12
      backend/i18n/frontend_nl.json
  6. 12
      backend/i18n/frontend_pt.json
  7. 12
      backend/i18n/frontend_zh.json
  8. 12
      backend/i18n/source/frontend_en.json
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs
  11. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  12. 30
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs
  13. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/CustomFluidParser.cs
  15. 32
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  16. 12
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  17. 57
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs
  20. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs
  21. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs
  23. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs
  24. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs
  25. 5
      backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
  26. 40
      backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  27. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHeaders.cs
  28. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupHandlerFactory.cs
  29. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/StreamMapper.cs
  31. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
  32. 34
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs
  33. 103
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  34. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs
  35. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs
  36. 19
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs
  37. 71
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  38. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  39. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs
  40. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs
  41. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs
  42. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs
  43. 15
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs
  44. 15
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs
  45. 26
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/OptimizeFieldQueriesDirective.cs
  46. 13
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs
  47. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
  48. 166
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs
  49. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs
  50. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  51. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs
  52. 48
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  53. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs
  54. 6
      backend/src/Squidex.Domain.Apps.Entities/ContextHeaders.cs
  55. 2
      backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  56. 23
      backend/src/Squidex.Domain.Apps.Entities/Q.cs
  57. 4
      backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  58. 2
      backend/src/Squidex.Domain.Users/UserWithClaims.cs
  59. 1
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs
  60. 18
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
  61. 4
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/ProfilerCollection.cs
  62. 2
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs
  63. 2
      backend/src/Squidex.Infrastructure/Commands/DomainObject.cs
  64. 14
      backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs
  65. 2
      backend/src/Squidex.Infrastructure/Json/System/Constants.cs
  66. 6
      backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs
  67. 2
      backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs
  68. 2
      backend/src/Squidex.Infrastructure/Queries/Query.cs
  69. 2
      backend/src/Squidex.Infrastructure/Queries/SortNode.cs
  70. 6
      backend/src/Squidex.Infrastructure/RandomHash.cs
  71. 2
      backend/src/Squidex.Infrastructure/Security/PermissionSet.cs
  72. 5
      backend/src/Squidex.Web/ContextProvider.cs
  73. 6
      backend/src/Squidex.Web/Pipeline/JsonStreamResult.cs
  74. 101
      backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeader.cs
  75. 99
      backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeaderAttribute.cs
  76. 8
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  77. 87
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  78. 24
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  79. 2
      backend/src/Squidex/Areas/IdentityServer/Config/AlwaysAddScopeHandler.cs
  80. 3
      backend/src/Squidex/Config/Domain/BackupsServices.cs
  81. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  82. 36
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs
  83. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs
  84. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/EnrichForCachingTests.cs
  85. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs
  86. 157
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  87. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  88. 41
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs
  89. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs
  90. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs
  91. 2
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorIntegrationTests.cs
  92. 2
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs
  93. 4
      backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs
  94. 12
      backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs
  95. 192
      frontend/src/app/features/content/pages/contents/contents-page.component.html
  96. 72
      frontend/src/app/features/content/pages/contents/contents-page.component.ts
  97. 4
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.html
  98. 8
      frontend/src/app/features/schemas/pages/schema/ui/field-list.component.html
  99. 9
      frontend/src/app/shared/services/contents.service.ts
  100. 25
      frontend/src/app/shared/services/schemas.service.ts

5
backend/.editorconfig

@ -172,4 +172,7 @@ dotnet_diagnostic.SA1602.severity = none
dotnet_diagnostic.SA1615.severity = none
# SA1623: Property summary documentation should match accessors
dotnet_diagnostic.SA1623.severity = none
dotnet_diagnostic.SA1623.severity = none
# xUnit1033: Test classes decorated with 'Xunit.IClassFixture<TFixture>' or 'Xunit.ICollectionFixture<TFixture>' should add a constructor argument of type TFixture
dotnet_diagnostic.xUnit1033.severity = none

12
backend/i18n/frontend_en.json

@ -986,17 +986,29 @@
"schemas.tabFields": "Fields",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Created",
"schemas.tableHeaders.created_title": "Created",
"schemas.tableHeaders.createdBy": "Created By",
"schemas.tableHeaders.createdBy_title": "Created By",
"schemas.tableHeaders.createdByShort": "By",
"schemas.tableHeaders.createdByShort_title": "Created By (Avatar only)",
"schemas.tableHeaders.id": "Id",
"schemas.tableHeaders.id_title": "Id",
"schemas.tableHeaders.lastModified": "Updated",
"schemas.tableHeaders.lastModified_title": "Updated",
"schemas.tableHeaders.lastModifiedBy": "Updated By",
"schemas.tableHeaders.lastModifiedBy_title": "Updated By",
"schemas.tableHeaders.lastModifiedByShort": "By",
"schemas.tableHeaders.lastModifiedByShort_title": "Updated By (Avatar only)",
"schemas.tableHeaders.nextStatus": "Next Status",
"schemas.tableHeaders.nextStatus_title": "Next Status",
"schemas.tableHeaders.status": "Status",
"schemas.tableHeaders.status_title": "Status",
"schemas.tableHeaders.translationStatus": "Translation Status",
"schemas.tableHeaders.translationStatus_title": "Translation Status (current Language)",
"schemas.tableHeaders.translationStatusAverage": "Average Translation Status",
"schemas.tableHeaders.translationStatusAverage_title": "Translation Status (average across Languages)",
"schemas.tableHeaders.version": "Version",
"schemas.tableHeaders.version_title": "Version",
"schemas.tabMore": "More",
"schemas.tabScripts": "Scripts",
"schemas.tabUI": "UI",

12
backend/i18n/frontend_fr.json

@ -986,17 +986,29 @@
"schemas.tabFields": "Des champs",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Créé",
"schemas.tableHeaders.created_title": "Created",
"schemas.tableHeaders.createdBy": "Créé par",
"schemas.tableHeaders.createdBy_title": "Created By",
"schemas.tableHeaders.createdByShort": "Par",
"schemas.tableHeaders.createdByShort_title": "Created By (Avatar only)",
"schemas.tableHeaders.id": "Identifiant",
"schemas.tableHeaders.id_title": "Id",
"schemas.tableHeaders.lastModified": "Mis à jour",
"schemas.tableHeaders.lastModified_title": "Updated",
"schemas.tableHeaders.lastModifiedBy": "Mis à jour par",
"schemas.tableHeaders.lastModifiedBy_title": "Updated By",
"schemas.tableHeaders.lastModifiedByShort": "Par",
"schemas.tableHeaders.lastModifiedByShort_title": "Updated By (Avatar only)",
"schemas.tableHeaders.nextStatus": "Statut suivant",
"schemas.tableHeaders.nextStatus_title": "Next Status",
"schemas.tableHeaders.status": "Statut",
"schemas.tableHeaders.status_title": "Status",
"schemas.tableHeaders.translationStatus": "Statut de la traduction",
"schemas.tableHeaders.translationStatus_title": "Translation Status (current Language)",
"schemas.tableHeaders.translationStatusAverage": "État de traduction moyen",
"schemas.tableHeaders.translationStatusAverage_title": "Translation Status (average across Languages)",
"schemas.tableHeaders.version": "Version",
"schemas.tableHeaders.version_title": "Version",
"schemas.tabMore": "Plus",
"schemas.tabScripts": "Scénarios",
"schemas.tabUI": "interface utilisateur",

12
backend/i18n/frontend_it.json

@ -986,17 +986,29 @@
"schemas.tabFields": "Campi",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Creato",
"schemas.tableHeaders.created_title": "Created",
"schemas.tableHeaders.createdBy": "Creato da",
"schemas.tableHeaders.createdBy_title": "Created By",
"schemas.tableHeaders.createdByShort": "Da",
"schemas.tableHeaders.createdByShort_title": "Created By (Avatar only)",
"schemas.tableHeaders.id": "Id",
"schemas.tableHeaders.id_title": "Id",
"schemas.tableHeaders.lastModified": "Modificato",
"schemas.tableHeaders.lastModified_title": "Updated",
"schemas.tableHeaders.lastModifiedBy": "Modificato da",
"schemas.tableHeaders.lastModifiedBy_title": "Updated By",
"schemas.tableHeaders.lastModifiedByShort": "Da",
"schemas.tableHeaders.lastModifiedByShort_title": "Updated By (Avatar only)",
"schemas.tableHeaders.nextStatus": "Stato successivo",
"schemas.tableHeaders.nextStatus_title": "Next Status",
"schemas.tableHeaders.status": "Stato",
"schemas.tableHeaders.status_title": "Status",
"schemas.tableHeaders.translationStatus": "Translation Status",
"schemas.tableHeaders.translationStatus_title": "Translation Status (current Language)",
"schemas.tableHeaders.translationStatusAverage": "Average Translation Status",
"schemas.tableHeaders.translationStatusAverage_title": "Translation Status (average across Languages)",
"schemas.tableHeaders.version": "Versione",
"schemas.tableHeaders.version_title": "Version",
"schemas.tabMore": "Altro",
"schemas.tabScripts": "Script",
"schemas.tabUI": "UI",

12
backend/i18n/frontend_nl.json

@ -986,17 +986,29 @@
"schemas.tabFields": "Velden",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Gemaakt",
"schemas.tableHeaders.created_title": "Created",
"schemas.tableHeaders.createdBy": "Gemaakt door",
"schemas.tableHeaders.createdBy_title": "Created By",
"schemas.tableHeaders.createdByShort": "Door",
"schemas.tableHeaders.createdByShort_title": "Created By (Avatar only)",
"schemas.tableHeaders.id": "Id",
"schemas.tableHeaders.id_title": "Id",
"schemas.tableHeaders.lastModified": "Bijgewerkt",
"schemas.tableHeaders.lastModified_title": "Updated",
"schemas.tableHeaders.lastModifiedBy": "Bijgewerkt door",
"schemas.tableHeaders.lastModifiedBy_title": "Updated By",
"schemas.tableHeaders.lastModifiedByShort": "Door",
"schemas.tableHeaders.lastModifiedByShort_title": "Updated By (Avatar only)",
"schemas.tableHeaders.nextStatus": "Volgende status",
"schemas.tableHeaders.nextStatus_title": "Next Status",
"schemas.tableHeaders.status": "Status",
"schemas.tableHeaders.status_title": "Status",
"schemas.tableHeaders.translationStatus": "Translation Status",
"schemas.tableHeaders.translationStatus_title": "Translation Status (current Language)",
"schemas.tableHeaders.translationStatusAverage": "Average Translation Status",
"schemas.tableHeaders.translationStatusAverage_title": "Translation Status (average across Languages)",
"schemas.tableHeaders.version": "Versie",
"schemas.tableHeaders.version_title": "Version",
"schemas.tabMore": "Meer",
"schemas.tabScripts": "Scripts",
"schemas.tabUI": "UI",

12
backend/i18n/frontend_pt.json

@ -986,17 +986,29 @@
"schemas.tabFields": "Campos",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Criado",
"schemas.tableHeaders.created_title": "Created",
"schemas.tableHeaders.createdBy": "Criado por",
"schemas.tableHeaders.createdBy_title": "Created By",
"schemas.tableHeaders.createdByShort": "Por",
"schemas.tableHeaders.createdByShort_title": "Created By (Avatar only)",
"schemas.tableHeaders.id": "Id",
"schemas.tableHeaders.id_title": "Id",
"schemas.tableHeaders.lastModified": "Actualizado",
"schemas.tableHeaders.lastModified_title": "Updated",
"schemas.tableHeaders.lastModifiedBy": "Actualizado por",
"schemas.tableHeaders.lastModifiedBy_title": "Updated By",
"schemas.tableHeaders.lastModifiedByShort": "Por",
"schemas.tableHeaders.lastModifiedByShort_title": "Updated By (Avatar only)",
"schemas.tableHeaders.nextStatus": "Próximo Estado",
"schemas.tableHeaders.nextStatus_title": "Next Status",
"schemas.tableHeaders.status": "Estado",
"schemas.tableHeaders.status_title": "Status",
"schemas.tableHeaders.translationStatus": "Estado da Tradução",
"schemas.tableHeaders.translationStatus_title": "Translation Status (current Language)",
"schemas.tableHeaders.translationStatusAverage": "Estado médio da tradução",
"schemas.tableHeaders.translationStatusAverage_title": "Translation Status (average across Languages)",
"schemas.tableHeaders.version": "Versão",
"schemas.tableHeaders.version_title": "Version",
"schemas.tabMore": "Mais",
"schemas.tabScripts": "Scripts",
"schemas.tabUI": "UI",

12
backend/i18n/frontend_zh.json

@ -986,17 +986,29 @@
"schemas.tabFields": "字段",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "创建",
"schemas.tableHeaders.created_title": "Created",
"schemas.tableHeaders.createdBy": "创建者",
"schemas.tableHeaders.createdBy_title": "Created By",
"schemas.tableHeaders.createdByShort": "By",
"schemas.tableHeaders.createdByShort_title": "Created By (Avatar only)",
"schemas.tableHeaders.id": "Id",
"schemas.tableHeaders.id_title": "Id",
"schemas.tableHeaders.lastModified": "更新",
"schemas.tableHeaders.lastModified_title": "Updated",
"schemas.tableHeaders.lastModifiedBy": "更新者",
"schemas.tableHeaders.lastModifiedBy_title": "Updated By",
"schemas.tableHeaders.lastModifiedByShort": "By",
"schemas.tableHeaders.lastModifiedByShort_title": "Updated By (Avatar only)",
"schemas.tableHeaders.nextStatus": "下一个状态",
"schemas.tableHeaders.nextStatus_title": "Next Status",
"schemas.tableHeaders.status": "状态",
"schemas.tableHeaders.status_title": "Status",
"schemas.tableHeaders.translationStatus": "Translation Status",
"schemas.tableHeaders.translationStatus_title": "Translation Status (current Language)",
"schemas.tableHeaders.translationStatusAverage": "Average Translation Status",
"schemas.tableHeaders.translationStatusAverage_title": "Translation Status (average across Languages)",
"schemas.tableHeaders.version": "版本",
"schemas.tableHeaders.version_title": "Version",
"schemas.tabMore": "More",
"schemas.tabScripts": "Scripts",
"schemas.tabUI": "UI",

12
backend/i18n/source/frontend_en.json

@ -986,17 +986,29 @@
"schemas.tabFields": "Fields",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Created",
"schemas.tableHeaders.created_title": "Created",
"schemas.tableHeaders.createdBy": "Created By",
"schemas.tableHeaders.createdBy_title": "Created By",
"schemas.tableHeaders.createdByShort": "By",
"schemas.tableHeaders.createdByShort_title": "Created By (Avatar only)",
"schemas.tableHeaders.id": "Id",
"schemas.tableHeaders.id_title": "Id",
"schemas.tableHeaders.lastModified": "Updated",
"schemas.tableHeaders.lastModified_title": "Updated",
"schemas.tableHeaders.lastModifiedBy": "Updated By",
"schemas.tableHeaders.lastModifiedBy_title": "Updated By",
"schemas.tableHeaders.lastModifiedByShort": "By",
"schemas.tableHeaders.lastModifiedByShort_title": "Updated By (Avatar only)",
"schemas.tableHeaders.nextStatus": "Next Status",
"schemas.tableHeaders.nextStatus_title": "Next Status",
"schemas.tableHeaders.status": "Status",
"schemas.tableHeaders.status_title": "Status",
"schemas.tableHeaders.translationStatus": "Translation Status",
"schemas.tableHeaders.translationStatus_title": "Translation Status (current Language)",
"schemas.tableHeaders.translationStatusAverage": "Average Translation Status",
"schemas.tableHeaders.translationStatusAverage_title": "Translation Status (average across Languages)",
"schemas.tableHeaders.version": "Version",
"schemas.tableHeaders.version_title": "Version",
"schemas.tabMore": "More",
"schemas.tabScripts": "Scripts",
"schemas.tabUI": "UI",

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
public static class JsonMapper
{
private class JsonObjectInstance : ObjectInstance
private sealed class JsonObjectInstance : ObjectInstance
{
public JsonObjectInstance(Engine engine)
: base(engine)

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs

@ -35,7 +35,7 @@ public static class JintExtensions
return ids;
}
internal static ScriptExecutionContext<T> ExtendAsync<T>(this ScriptExecutionContext<T> context,
internal static ScriptExecutionContext<T> ExtendWithAsyncFunctions<T>(this ScriptExecutionContext<T> context,
IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
@ -46,7 +46,7 @@ public static class JintExtensions
return context;
}
internal static ScriptExecutionContext<T> Extend<T>(this ScriptExecutionContext<T> context,
internal static ScriptExecutionContext<T> ExtendWithFunctions<T>(this ScriptExecutionContext<T> context,
IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
@ -57,7 +57,7 @@ public static class JintExtensions
return context;
}
internal static ScriptExecutionContext<T> Extend<T>(this ScriptExecutionContext<T> context,
internal static ScriptExecutionContext<T> ExtendWithVariables<T>(this ScriptExecutionContext<T> context,
ScriptVars vars,
ScriptOptions options)
{

26
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -53,10 +53,10 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
combined.CancelAfter(timeoutExecution);
var context =
CreateEngine<JsonValue?>(options, combined.Token)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions);
CreateEngine<JsonValue>(options, combined.Token)
.ExtendWithVariables(vars, options)
.ExtendWithFunctions(extensions)
.ExtendWithAsyncFunctions(extensions);
context.Engine.SetValue("complete", new Action<JsValue?>(value =>
{
@ -65,7 +65,7 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
var result = Execute(context, script);
return await context.CompleteAsync() ?? JsonMapper.Map(result);
return await context.WaitForCompletionAsync(() => JsonMapper.Map(result));
}
catch (Exception ex)
{
@ -79,6 +79,8 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
Guard.NotNull(vars);
Guard.NotNullOrEmpty(script);
var data = vars.Data!;
using var combined = CancellationTokenSource.CreateLinkedTokenSource(ct);
try
{
@ -87,13 +89,13 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
var context =
CreateEngine<ContentData>(options, combined.Token)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions);
.ExtendWithVariables(vars, options)
.ExtendWithFunctions(extensions)
.ExtendWithAsyncFunctions(extensions);
context.Engine.SetValue("complete", new Action<JsValue?>(_ =>
{
context.Complete(vars.Data!);
context.Complete(data!);
}));
context.Engine.SetValue("replace", new Action(() =>
@ -111,7 +113,7 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
Execute(context, script);
return await context.CompleteAsync() ?? vars.Data!;
return await context.WaitForCompletionAsync(() => data);
}
catch (Exception ex)
{
@ -128,8 +130,8 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
{
var context =
CreateEngine<object>(options, default)
.Extend(vars, options)
.Extend(extensions);
.ExtendWithVariables(vars, options)
.ExtendWithFunctions(extensions);
var result = Execute(context, script);

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

@ -21,17 +21,22 @@ public abstract class ScriptExecutionContext : ScriptContext
Engine = engine;
}
public abstract JsValue Evaluate(Script program);
public abstract JsValue Evaluate(Script script);
public abstract void Schedule(Func<IScheduler, CancellationToken, Task> action);
}
public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, IScheduler
{
private readonly TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
private readonly TaskCompletionSource<CompletedValue?> tcs = new TaskCompletionSource<CompletedValue?>();
private readonly CancellationToken cancellationToken;
private int pendingTasks = 1;
private sealed class CompletedValue
{
public T Value { get; init; }
}
public bool IsCompleted
{
get => tcs.Task.Status is TaskStatus.RanToCompletion or TaskStatus.Faulted;
@ -43,16 +48,23 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
this.cancellationToken = cancellationToken;
}
public Task<T> CompleteAsync()
public async Task<T> WaitForCompletionAsync(Func<T> fallback)
{
TryComplete(default!);
TryComplete();
var result = await tcs.Task.WithCancellation(cancellationToken);
return tcs.Task.WithCancellation(cancellationToken);
if (result == null)
{
return fallback();
}
return result.Value;
}
public void Complete(T value)
{
tcs.TrySetResult(value);
tcs.TrySetResult(new CompletedValue { Value = value });
}
public override JsValue Evaluate(Script script)
@ -77,7 +89,7 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
{
await action(this, cancellationToken);
TryComplete(default!);
TryComplete();
}
catch (Exception ex)
{
@ -104,7 +116,7 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
action();
}
TryComplete(default!);
TryComplete();
}
catch (Exception ex)
{
@ -146,7 +158,7 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
Interlocked.Increment(ref pendingTasks);
}
private void TryComplete(T result)
private void TryComplete(CompletedValue? result = null)
{
if (Interlocked.Decrement(ref pendingTasks) <= 0)
{

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

@ -464,11 +464,6 @@ public sealed class ScriptingCompleter
Add(JsonType.Function, name, description);
}
private void AddObject(string? name, string? description)
{
Add(JsonType.Object, name, description);
}
private void AddString(string? name, string? description)
{
Add(JsonType.String, name, description);

2
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/CustomFluidParser.cs

@ -9,6 +9,8 @@ using Fluid;
using Fluid.Ast;
using Parlot.Fluent;
#pragma warning disable CA1822 // Mark members as static
namespace Squidex.Domain.Apps.Core.Templates;
public sealed class CustomFluidParser : FluidParser

32
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -9,6 +9,7 @@ using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.DomainObject;
using Squidex.Infrastructure;
@ -127,9 +128,10 @@ public sealed class MongoContentEntity : IContentEntity, IVersionedEntity<Domain
return state;
}
public static async Task<MongoContentEntity> CreatePublishedAsync(SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider)
public static async Task<MongoContentEntity> CreatePublishedAsync(SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider,
CancellationToken ct)
{
var entity = await CreateContentAsync(job.Value.CurrentVersion.Data, job, appProvider);
var entity = await CreateContentAsync(job.Value.CurrentVersion.Data, job, appProvider, ct);
entity.ScheduledAt = null;
entity.ScheduleJob = null;
@ -138,9 +140,10 @@ public sealed class MongoContentEntity : IContentEntity, IVersionedEntity<Domain
return entity;
}
public static async Task<MongoContentEntity> CreateCompleteAsync(SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider)
public static async Task<MongoContentEntity> CreateCompleteAsync(SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider,
CancellationToken ct)
{
var entity = await CreateContentAsync(job.Value.Data, job, appProvider);
var entity = await CreateContentAsync(job.Value.Data, job, appProvider, ct);
entity.ScheduledAt = job.Value.ScheduleJob?.DueTime;
entity.ScheduleJob = job.Value.ScheduleJob;
@ -151,7 +154,8 @@ public sealed class MongoContentEntity : IContentEntity, IVersionedEntity<Domain
return entity;
}
private static async Task<MongoContentEntity> CreateContentAsync(ContentData data, SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider)
private static async Task<MongoContentEntity> CreateContentAsync(ContentData data, SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider,
CancellationToken ct)
{
var entity = SimpleMapper.Map(job.Value, new MongoContentEntity());
@ -162,20 +166,22 @@ public sealed class MongoContentEntity : IContentEntity, IVersionedEntity<Domain
entity.ReferencedIds ??= new HashSet<DomainId>();
entity.Version = job.NewVersion;
var (app, schema) = await appProvider.GetAppWithSchemaAsync(job.Value.AppId.Id, job.Value.SchemaId.Id, true);
var (app, schema) = await appProvider.GetAppWithSchemaAsync(entity.IndexedAppId, entity.IndexedSchemaId, true, ct);
if (schema?.SchemaDef != null && app != null)
if (app == null || schema == null)
{
if (data.CanHaveReference())
{
var components = await appProvider.GetComponentsAsync(schema);
return entity;
}
entity.Data.AddReferencedIds(schema.SchemaDef, entity.ReferencedIds, components);
}
if (data.CanHaveReference())
{
var components = await appProvider.GetComponentsAsync(schema, ct: ct);
entity.TranslationStatus = TranslationStatus.Create(data, schema.SchemaDef, app.Languages);
entity.Data.AddReferencedIds(schema.SchemaDef, entity.ReferencedIds, components);
}
entity.TranslationStatus = TranslationStatus.Create(data, schema.SchemaDef, app.Languages);
return entity;
}
}

12
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -133,13 +133,13 @@ public partial class MongoContentRepository : ISnapshotStore<ContentDomainObject
if (isValid && ShouldWritePublished(job.Value))
{
await collectionPublished.AddCollectionsAsync(
await MongoContentEntity.CreatePublishedAsync(job, appProvider), add, ct);
await MongoContentEntity.CreatePublishedAsync(job, appProvider, ct), add, ct);
}
if (isValid)
{
await collectionComplete.AddCollectionsAsync(
await MongoContentEntity.CreateCompleteAsync(job, appProvider), add, ct);
await MongoContentEntity.CreateCompleteAsync(job, appProvider, ct), add, ct);
}
}
@ -163,7 +163,7 @@ public partial class MongoContentRepository : ISnapshotStore<ContentDomainObject
{
if (ShouldWritePublished(job.Value))
{
var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider));
var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider, ct));
await collectionPublished.UpsertAsync(entityJob, ct);
}
@ -178,7 +178,7 @@ public partial class MongoContentRepository : ISnapshotStore<ContentDomainObject
{
if (ShouldWritePublished(job.Value))
{
var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider));
var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider, ct));
await collectionPublished.UpsertVersionedAsync(session, entityJob, ct);
}
@ -191,7 +191,7 @@ public partial class MongoContentRepository : ISnapshotStore<ContentDomainObject
private async Task UpsertCompleteAsync(SnapshotWriteJob<ContentDomainObject.State> job,
CancellationToken ct)
{
var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider));
var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider, ct));
await collectionComplete.UpsertAsync(entityJob, ct);
}
@ -199,7 +199,7 @@ public partial class MongoContentRepository : ISnapshotStore<ContentDomainObject
private async Task UpsertVersionedCompleteAsync(IClientSessionHandle session, SnapshotWriteJob<ContentDomainObject.State> job,
CancellationToken ct)
{
var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider));
var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider, ct));
await collectionComplete.UpsertVersionedAsync(session, entityJob, ct);
}

57
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
@ -17,6 +18,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations;
public static class Extensions
{
private static Dictionary<string, string> propertyMap;
public static IReadOnlyDictionary<string, string> PropertyMap
{
get => propertyMap ??=
BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps
.Where(x =>
x.MemberName != nameof(MongoContentEntity.DraftData) &&
x.MemberName != nameof(MongoContentEntity.Data))
.ToDictionary(
x => x.MemberName,
x => x.ElementName,
StringComparer.OrdinalIgnoreCase);
}
public sealed class StatusOnly
{
[BsonId]
@ -56,7 +72,7 @@ public static class Extensions
query.Sort[1].Order == SortOrder.Ascending;
}
public static async Task<List<MongoContentEntity>> QueryContentsAsync(this IMongoCollection<MongoContentEntity> collection, FilterDefinition<MongoContentEntity> filter, ClrQuery query,
public static async Task<List<MongoContentEntity>> QueryContentsAsync(this IMongoCollection<MongoContentEntity> collection, FilterDefinition<MongoContentEntity> filter, ClrQuery query, Q q,
CancellationToken ct)
{
if (query.Skip > 0 && !query.IsSatisfiedByIndex())
@ -98,6 +114,7 @@ public static class Extensions
.QuerySkip(query)
.QueryLimit(query)
.Lookup<IdOnly, MongoContentEntity, IdOnly>(collection, x => x.Id, x => x.DocumentId, x => x.Joined)
.SelectFields(q.Fields)
.ToListAsync(ct);
return joined.Select(x => x.Joined[0]).ToList();
@ -108,6 +125,7 @@ public static class Extensions
.QuerySort(query)
.QuerySkip(query)
.QueryLimit(query)
.SelectFields(q.Fields)
.ToListRandomAsync(collection, query.Random, ct);
return await result;
@ -126,4 +144,41 @@ public static class Extensions
.Include(x => x.Status))
.ToListAsync(ct);
}
public static IFindFluent<T, T> SelectFields<T>(this IFindFluent<T, T> find, IEnumerable<string>? fields)
{
return find.Project<T>(BuildProjection<T>(fields));
}
public static IAggregateFluent<T> SelectFields<T>(this IAggregateFluent<T> find, IEnumerable<string>? fields)
{
return find.Project<T>(BuildProjection<T>(fields));
}
private static ProjectionDefinition<T> BuildProjection<T>(IEnumerable<string>? fields)
{
var projector = Builders<T>.Projection;
var projections = new List<ProjectionDefinition<T>>();
if (fields?.Any() == true)
{
var dataField = Field.Of<MongoContentEntity>(x => nameof(x.Data));
foreach (var field in fields)
{
projections.Add(projector.Include($"{dataField}.{field}"));
}
foreach (var field in PropertyMap.Values)
{
projections.Add(projector.Include(field));
}
}
else
{
projections.Add(projector.Exclude(Field.Of<MongoContentEntity>(x => nameof(x.DraftData))));
}
return projector.Combine(projections);
}
}

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs

@ -19,7 +19,7 @@ public sealed class QueryAsStream : OperationBase
{
var filter = CreateFilter(appId, schemaIds);
using (var cursor = await Collection.Find(filter).ToCursorAsync(ct))
using (var cursor = await Collection.Find(filter).SelectFields(null).ToCursorAsync(ct))
{
while (await cursor.MoveNextAsync(ct))
{

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs

@ -19,7 +19,7 @@ internal sealed class QueryById : OperationBase
{
var filter = Filter.Eq(x => x.DocumentId, DomainId.Combine(schema.AppId, id));
var contentEntity = await Collection.Find(filter).FirstOrDefaultAsync(ct);
var contentEntity = await Collection.Find(filter).SelectFields(null).FirstOrDefaultAsync(ct);
if (contentEntity == null || contentEntity.IndexedSchemaId != schema.Id)
{

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs

@ -50,7 +50,7 @@ internal sealed class QueryByIds : OperationBase
// Create a filter from the Ids and ensure that the content ids match to the schema IDs.
var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet(), query.Filter);
var contentEntities = await FindContentsAsync(query, filter, ct);
var contentEntities = await FindContentsAsync(query, filter, q, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= query.Take || query.Skip > 0)
@ -68,7 +68,7 @@ internal sealed class QueryByIds : OperationBase
return ResultList.Create(contentTotal, contentEntities);
}
private async Task<List<MongoContentEntity>> FindContentsAsync(ClrQuery query, FilterDefinition<MongoContentEntity> filter,
private async Task<List<MongoContentEntity>> FindContentsAsync(ClrQuery query, FilterDefinition<MongoContentEntity> filter, Q q,
CancellationToken ct)
{
var result =
@ -76,6 +76,7 @@ internal sealed class QueryByIds : OperationBase
.QuerySort(query)
.QuerySkip(query)
.QueryLimit(query)
.SelectFields(q.Fields)
.ToListRandomAsync(Collection, query.Random, ct);
return await result;

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs

@ -64,7 +64,7 @@ internal sealed class QueryByQuery : OperationBase
var (filter, isDefault) = CreateFilter(app.Id, schemas.Select(x => x.Id), query, q.Reference, q.CreatedBy);
var contentEntities = await Collection.QueryContentsAsync(filter, query, ct);
var contentEntities = await Collection.QueryContentsAsync(filter, query, q, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= query.Take || query.Skip > 0)
@ -96,7 +96,7 @@ internal sealed class QueryByQuery : OperationBase
// Default means that no other filters are applied and we only query by app and schema.
var (filter, isDefault) = CreateFilter(schema.AppId.Id, Enumerable.Repeat(schema.Id, 1), query, q.Reference, q.CreatedBy);
var contentEntities = await Collection.QueryContentsAsync(filter, query, ct);
var contentEntities = await Collection.QueryContentsAsync(filter, query, q, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= query.Take || query.Skip > 0)

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs

@ -88,7 +88,7 @@ internal sealed class QueryInDedicatedCollection : MongoBase<MongoContentEntity>
var filter = CreateFilter(query, q.Reference, q.CreatedBy);
var contentCollection = await GetCollectionAsync(schema.AppId.Id, schema.Id);
var contentEntities = await contentCollection.QueryContentsAsync(filter, query, ct);
var contentEntities = await contentCollection.QueryContentsAsync(filter, query, q, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= query.Take || query.Skip > 0)

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs

@ -40,7 +40,7 @@ internal sealed class QueryReferrers : OperationBase
{
var filter = BuildFilter(appId, reference);
using (var cursor = await Collection.Find(filter).Limit(take).ToCursorAsync(ct))
using (var cursor = await Collection.Find(filter).Limit(take).SelectFields(null).ToCursorAsync(ct))
{
while (await cursor.MoveNextAsync(ct))
{

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs

@ -33,7 +33,7 @@ internal sealed class QueryScheduled : OperationBase
{
var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.ScheduledFrom!.Value, q.ScheduledTo!.Value);
var contentEntities = await Collection.Find(filter).Limit(100).ToListAsync(ct);
var contentEntities = await Collection.Find(filter).Limit(100).SelectFields(q.Fields).ToListAsync(ct);
var contentTotal = (long)contentEntities.Count;
return ResultList.Create(contentTotal, contentEntities);
@ -42,7 +42,7 @@ internal sealed class QueryScheduled : OperationBase
public IAsyncEnumerable<IContentEntity> QueryAsync(Instant now,
CancellationToken ct)
{
var find = Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true).Not(x => x.Data);
var find = Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true).Not(x => x.Data, x => x.DraftData);
return find.ToAsyncEnumerable(ct);
}

5
backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -213,6 +213,11 @@ public sealed class AppProvider : IAppProvider
return rules.Find(x => x.Id == id);
}
public void RegisterAppForLocalContext(DomainId appId, IAppEntity app)
{
localCache.Add(AppCacheKey(appId), app);
}
public async Task<T?> GetOrCreate<T>(object key, Func<Task<T?>> creator) where T : class
{
if (localCache.TryGetValue(key, out var value))

40
backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs

@ -23,8 +23,10 @@ public sealed class BackupApps : IBackupHandler
private const string AvatarFile = "Avatar.image";
private readonly Rebuilder rebuilder;
private readonly IAppImageStore appImageStore;
private readonly IAppProvider appProvider;
private readonly IAppsIndex appsIndex;
private readonly IAppUISettings appUISettings;
private AppDomainObject? app;
private string? appReservation;
public string Name { get; } = "Apps";
@ -32,11 +34,13 @@ public sealed class BackupApps : IBackupHandler
public BackupApps(
Rebuilder rebuilder,
IAppImageStore appImageStore,
IAppProvider appProvider,
IAppsIndex appsIndex,
IAppUISettings appUISettings)
{
this.appsIndex = appsIndex;
this.appImageStore = appImageStore;
this.appProvider = appProvider;
this.appUISettings = appUISettings;
this.rebuilder = rebuilder;
}
@ -69,18 +73,12 @@ public sealed class BackupApps : IBackupHandler
switch (@event.Payload)
{
case AppCreated appCreated:
{
await ReserveAppAsync(context.AppId, appCreated.Name, ct);
break;
}
await ReserveAppAsync(context.AppId, appCreated.Name, ct);
break;
case AppImageUploaded:
{
await ReadAssetAsync(context.AppId, context.Reader, ct);
break;
}
await ReadAssetAsync(context.AppId, context.Reader, ct);
break;
case AppContributorAssigned contributorAssigned:
{
@ -110,6 +108,21 @@ public sealed class BackupApps : IBackupHandler
public async Task RestoreAsync(RestoreContext context,
CancellationToken ct)
{
await RestoreAppTemporarilyAsync(context, ct);
await RestoreSettingsAsync(context, ct);
}
private async Task RestoreAppTemporarilyAsync(RestoreContext context,
CancellationToken ct)
{
app = await rebuilder.RebuildStateAsync<AppDomainObject, AppDomainObject.State>(context.AppId, ct);
// The app is only available in the context of the restore, so there is nothing to clear.
appProvider.RegisterAppForLocalContext(context.AppId, app!.Snapshot);
}
private async Task RestoreSettingsAsync(RestoreContext context, CancellationToken ct)
{
var json = await context.Reader.ReadJsonAsync<JsonObject>(SettingsFile, ct);
@ -134,7 +147,10 @@ public sealed class BackupApps : IBackupHandler
public async Task CompleteRestoreAsync(RestoreContext context, string appName)
{
await rebuilder.InsertManyAsync<AppDomainObject, AppDomainObject.State>(Enumerable.Repeat(context.AppId, 1), 1, default);
if (app != null)
{
await app.RebuildStateAsync();
}
await appsIndex.RemoveReservationAsync(appReservation);
}
@ -144,6 +160,7 @@ public sealed class BackupApps : IBackupHandler
{
try
{
// Ignore errors when writing assets, because they are not that relevant.
await using (var stream = await writer.OpenBlobAsync(AvatarFile, ct))
{
await appImageStore.DownloadAsync(appId, stream, ct);
@ -159,6 +176,7 @@ public sealed class BackupApps : IBackupHandler
{
try
{
// Ignore errors when writing assets, because they are not that relevant.
await using (var stream = await reader.OpenBlobAsync(AvatarFile, ct))
{
await appImageStore.UploadAsync(appId, stream, ct);

6
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHeaders.cs

@ -9,15 +9,15 @@ namespace Squidex.Domain.Apps.Entities.Assets;
public static class AssetHeaders
{
public const string HeaderNoEnrichment = "X-NoAssetEnrichment";
public const string NoEnrichment = "X-NoAssetEnrichment";
public static bool ShouldSkipAssetEnrichment(this Context context)
{
return context.Headers.ContainsKey(HeaderNoEnrichment);
return context.Headers.ContainsKey(NoEnrichment);
}
public static ICloneBuilder WithoutAssetEnrichment(this ICloneBuilder builder, bool value = true)
{
return builder.WithBoolean(HeaderNoEnrichment, value);
return builder.WithBoolean(NoEnrichment, value);
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupHandlerFactory.cs

@ -20,6 +20,6 @@ public sealed class DefaultBackupHandlerFactory : IBackupHandlerFactory
public IEnumerable<IBackupHandler> CreateMany()
{
return serviceProvider.GetRequiredService<IEnumerable<IBackupHandler>>();
return serviceProvider.GetRequiredService<IEnumerable<IBackupHandler>>().OrderBy(x => x.Order);
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs

@ -14,6 +14,8 @@ public interface IBackupHandler
{
string Name { get; }
int Order => 0;
public Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context,
CancellationToken ct)
{

2
backend/src/Squidex.Domain.Apps.Entities/Backup/StreamMapper.cs

@ -28,7 +28,7 @@ public sealed class StreamMapper
{
Guard.NotNullOrEmpty(stream);
var typeIndex = stream.IndexOf("-", StringComparison.Ordinal);
var typeIndex = stream.IndexOf('-', StringComparison.Ordinal);
var typeName = stream[..typeIndex];
var id = DomainId.Create(stream[(typeIndex + 1)..]);

3
backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs

@ -31,6 +31,8 @@ public sealed class BackupContents : IBackupHandler
public string Name { get; } = "Contents";
public int Order => int.MaxValue;
public sealed class Urls
{
public string Assets { get; set; }
@ -190,7 +192,6 @@ public sealed class BackupContents : IBackupHandler
CancellationToken ct)
{
var ids = contentIdsBySchemaId.Values.SelectMany(x => x);
if (ids.Any())
{
await rebuilder.InsertManyAsync<ContentDomainObject, ContentDomainObject.State>(ids, BatchSize, ct);

34
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs

@ -17,17 +17,19 @@ public static class ContentHeaders
{
private static readonly char[] Separators = { ',', ';' };
public const string Fields = "X-Fields";
public const string Flatten = "X-Flatten";
public const string Languages = "X-Languages";
public const string NoCleanup = "X-NoCleanup";
public const string NoEnrichment = "X-NoEnrichment";
public const string NoResolveLanguages = "X-NoResolveLanguages";
public const string ResolveFlow = "X-ResolveFlow";
public const string ResolveUrls = "X-Resolve-Urls";
public const string ResolveUrls = "X-ResolveUrls";
public const string Unpublished = "X-Unpublished";
public static void AddCacheHeaders(this Context context, IRequestCache cache)
{
cache.AddHeader(Fields);
cache.AddHeader(Flatten);
cache.AddHeader(Languages);
cache.AddHeader(NoCleanup);
@ -128,25 +130,33 @@ public static class ContentHeaders
return builder.WithStrings(ResolveUrls, fieldNames);
}
public static IEnumerable<Language> LanguageList(this Context context)
public static HashSet<string>? FieldsList(this Context context)
{
if (context.Headers.TryGetValue(Languages, out var value))
if (context.Headers.TryGetValue(Fields, out var value))
{
var languages = new HashSet<Language>();
return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).ToHashSet();
}
foreach (var iso2Code in value.Split(Separators, StringSplitOptions.RemoveEmptyEntries))
{
languages.Add(iso2Code);
}
return null;
}
return languages;
public static ICloneBuilder WithFields(this ICloneBuilder builder, IEnumerable<string> fields)
{
return builder.WithStrings(Fields, fields);
}
public static HashSet<Language> LanguagesList(this Context context)
{
if (context.Headers.TryGetValue(Languages, out var value))
{
return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => (Language)x).ToHashSet();
}
return Enumerable.Empty<Language>();
return new HashSet<Language>();
}
public static ICloneBuilder WithLanguages(this ICloneBuilder builder, IEnumerable<string> fieldNames)
public static ICloneBuilder WithLanguages(this ICloneBuilder builder, IEnumerable<string> languages)
{
return builder.WithStrings(Languages, fieldNames);
return builder.WithStrings(Languages, languages);
}
}

103
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -9,9 +9,10 @@ using GraphQL.DataLoader;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Users;
#pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL;
public sealed class GraphQLExecutionContext : QueryExecutionContext
@ -36,7 +37,8 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
Context = context.Clone(b => b
.WithoutCleanup()
.WithoutContentEnrichment());
.WithoutContentEnrichment()
.WithoutAssetEnrichment());
}
public async ValueTask<IUser?> FindUserAsync(RefToken refToken,
@ -54,37 +56,24 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
}
}
public async Task<IEnrichedAssetEntity?> FindAssetAsync(DomainId id,
public async Task<IEnrichedAssetEntity?> GetAssetAsync(DomainId id, TimeSpan cacheDuration,
CancellationToken ct)
{
var dataLoader = GetAssetsLoader();
var assets = await GetAssetsAsync(new List<DomainId> { id }, cacheDuration, ct);
var asset = assets.FirstOrDefault();
return await dataLoader.LoadAsync(id).GetResultAsync(ct);
return asset;
}
public async Task<IContentEntity?> FindContentAsync(DomainId schemaId, DomainId id,
public async Task<IEnrichedContentEntity?> GetContentAsync(DomainId schemaId, DomainId id, HashSet<string>? fields, TimeSpan cacheDuration,
CancellationToken ct)
{
var dataLoader = GetContentsLoader();
var content = await dataLoader.LoadAsync(id).GetResultAsync(ct);
if (content?.SchemaId.Id != schemaId)
{
content = null;
}
var contents = await GetContentsAsync(new List<DomainId> { id }, fields, cacheDuration, ct);
var content = contents.FirstOrDefault(x => x.SchemaId.Id == schemaId);
return content;
}
public Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(JsonValue value, TimeSpan cacheDuration,
CancellationToken ct)
{
var ids = ParseIds(value);
return GetAssetsAsync(ids, cacheDuration, ct);
}
public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetAssetsAsync(List<DomainId>? ids, TimeSpan cacheDuration,
CancellationToken ct)
{
@ -113,15 +102,7 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
return await LoadAsync(ids);
}
public Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(JsonValue value, TimeSpan cacheDuration,
CancellationToken ct)
{
var ids = ParseIds(value);
return GetContentsAsync(ids, cacheDuration, ct);
}
public async Task<IReadOnlyList<IEnrichedContentEntity>> GetContentsAsync(List<DomainId>? ids, TimeSpan cacheDuration,
public async Task<IReadOnlyList<IEnrichedContentEntity>> GetContentsAsync(List<DomainId>? ids, HashSet<string>? fields, TimeSpan cacheDuration,
CancellationToken ct)
{
if (ids == null || ids.Count == 0)
@ -129,24 +110,23 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
return EmptyContents;
}
async Task<IReadOnlyList<IEnrichedContentEntity>> LoadAsync(IEnumerable<DomainId> ids)
{
var result = await GetContentsLoader().LoadAsync(ids).GetResultAsync(ct);
return result?.NotNull().ToList() ?? EmptyContents;
}
if (cacheDuration > TimeSpan.Zero)
if (cacheDuration > TimeSpan.Zero || fields == null)
{
var contents = await ContentCache.CacheOrQueryAsync(ids, async pendingIds =>
{
return await LoadAsync(pendingIds);
var result = await GetContentsLoader().LoadAsync(ids).GetResultAsync(ct);
return result?.NotNull().ToList() ?? EmptyContents;
}, cacheDuration);
return contents.ToList();
}
else
{
var contents = await GetContentsLoaderWithFields().LoadAsync(ids.Select(x => (x, fields))).GetResultAsync(ct);
return await LoadAsync(ids);
return contents?.NotNull().ToList() ?? EmptyContents;
}
}
private IDataLoader<DomainId, IEnrichedAssetEntity> GetAssetsLoader()
@ -165,46 +145,33 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedContentEntity>(nameof(GetContentsLoader),
async (batch, ct) =>
{
var result = await QueryContentsByIdsAsync(new List<DomainId>(batch), ct);
var result = await QueryContentsByIdsAsync(batch, null, ct);
return result.ToDictionary(x => x.Id);
});
}
private IDataLoader<string, IUser> GetUserLoader()
private IDataLoader<(DomainId Id, HashSet<string> Fields), IEnrichedContentEntity> GetContentsLoaderWithFields()
{
return dataLoaders.Context!.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader),
return dataLoaders.Context!.GetOrAddBatchLoader<(DomainId Id, HashSet<string> Fields), IEnrichedContentEntity>(nameof(GetContentsLoader),
async (batch, ct) =>
{
var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct);
var fields = batch.SelectMany(x => x.Fields).ToHashSet();
return result;
var result = await QueryContentsByIdsAsync(batch.Select(x => x.Id), fields, ct);
return result.ToDictionary(x => (x.Id, fields));
});
}
private static List<DomainId>? ParseIds(JsonValue value)
private IDataLoader<string, IUser> GetUserLoader()
{
try
{
List<DomainId>? result = null;
if (value.Value is JsonArray a)
return dataLoaders.Context!.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader),
async (batch, ct) =>
{
foreach (var item in a)
{
if (item.Value is string id)
{
result ??= new List<DomainId>();
result.Add(DomainId.Create(id));
}
}
}
return result;
}
catch
{
return null;
}
var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct);
return result;
});
}
}

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs

@ -7,6 +7,7 @@
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
@ -79,7 +80,7 @@ internal sealed class ApplicationQueries : ObjectGraphType
{
var unionType = builder.GetContentUnion("AllContents", null);
if (!unionType.HasType)
if (unionType.SchemaTypes.Count == 0)
{
return;
}

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs

@ -61,7 +61,8 @@ internal static class AssetActions
{
var assetId = fieldContext.GetArgument<DomainId>("id");
return await context.FindAssetAsync(assetId,
return await context.GetAssetAsync(assetId,
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
});
}

19
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs

@ -48,6 +48,8 @@ internal sealed class Builder
public IInterfaceGraphType ComponentInterface { get; } = new ComponentInterfaceGraphType();
public List<SchemaInfo> Schemas { get; private set; }
public Builder(IAppEntity app, GraphQLOptions options)
{
partitionResolver = app.PartitionResolver();
@ -64,9 +66,9 @@ internal sealed class Builder
allSchemas.AddRange(SchemaInfo.Build(schemas, typeNames).Where(x => x.Fields.Count > 0));
// Only published normal schemas (not components are used for entities).
var schemaInfos = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList();
Schemas = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList();
foreach (var schemaInfo in schemaInfos)
foreach (var schemaInfo in Schemas)
{
var contentType = new ContentGraphType(schemaInfo);
@ -74,7 +76,7 @@ internal sealed class Builder
contentResultTypes[schemaInfo] = new ContentResultGraphType(contentType, schemaInfo);
}
foreach (var schemaInfo in allSchemas)
foreach (var schemaInfo in Schemas)
{
var componentType = new ComponentGraphType(schemaInfo);
@ -83,17 +85,18 @@ internal sealed class Builder
var newSchema = new GraphQLSchema
{
Query = new ApplicationQueries(this, schemaInfos)
Query = new ApplicationQueries(this, Schemas)
};
newSchema.RegisterType(ComponentInterface);
newSchema.RegisterType(ContentInterface);
newSchema.Directives.Register(SharedTypes.MemoryCacheDirective);
newSchema.Directives.Register(SharedTypes.CacheDirective);
newSchema.Directives.Register(SharedTypes.OptimizeFieldQueriesDirective);
if (schemaInfos.Any())
if (Schemas.Any())
{
var mutations = new ApplicationMutations(this, schemaInfos);
var mutations = new ApplicationMutations(this, Schemas);
if (mutations.Fields.Count > 0)
{
@ -108,7 +111,7 @@ internal sealed class Builder
foreach (var (schemaInfo, contentType) in contentTypes)
{
contentType.Initialize(this, schemaInfo, schemaInfos);
contentType.Initialize(this, schemaInfo, Schemas);
}
foreach (var (schemaInfo, componentType) in componentTypes)

71
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs

@ -8,12 +8,17 @@
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using GraphQLParser;
using GraphQLParser.AST;
using NodaTime;
using Squidex.CLI.Commands.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Subscriptions;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives;
using Squidex.Infrastructure;
using Squidex.Shared;
@ -79,18 +84,20 @@ internal static class ContentActions
public static readonly IFieldResolver Resolver = Resolvers.Async<object, object?>(async (_, fieldContext, context) =>
{
var contentId = fieldContext.GetArgument<DomainId>("id");
var contentSchema = fieldContext.FieldDefinition.SchemaId();
var version = fieldContext.GetArgument<int?>("version");
var contentSchemaId = fieldContext.FieldDefinition.SchemaId();
var contentVersion = fieldContext.GetArgument<int?>("version");
if (version >= 0)
if (contentVersion >= 0)
{
return await context.FindContentAsync(contentSchema, contentId, version.Value,
return await context.FindContentAsync(contentSchemaId.ToString(), contentId, contentVersion.Value,
fieldContext.CancellationToken);
}
else
{
return await context.FindContentAsync(DomainId.Create(contentSchema), contentId,
return await context.GetContentAsync(contentSchemaId, contentId,
fieldContext.FieldNames(),
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
}
});
@ -100,19 +107,22 @@ internal static class ContentActions
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(Scalars.Strings)
new QueryArgument(Scalars.NonNullStrings)
{
Name = "ids",
Description = FieldDescriptions.EntityIds,
DefaultValue = null
DefaultValue = null,
}
};
public static readonly IFieldResolver Resolver = Resolvers.Async<object, object?>(async (_, fieldContext, context) =>
{
var contentIds = fieldContext.GetArgument<DomainId[]>("ids");
var ids = fieldContext.GetArgument<DomainId[]>("ids").ToList();
return await context.QueryContentsByIdsAsync(contentIds, fieldContext.CancellationToken);
return await context.GetContentsAsync(ids,
fieldContext.FieldNames(),
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
});
}
@ -156,9 +166,12 @@ internal static class ContentActions
{
var query = fieldContext.BuildODataQuery();
var q = Q.Empty.WithODataQuery(query).WithoutTotal();
var q = Q.Empty
.WithODataQuery(query)
.WithoutTotal()
.WithFields(fieldContext.FieldNames());
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q,
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId().ToString(), q,
fieldContext.CancellationToken);
});
@ -166,9 +179,11 @@ internal static class ContentActions
{
var query = fieldContext.BuildODataQuery();
var q = Q.Empty.WithODataQuery(query);
var q = Q.Empty
.WithODataQuery(query)
.WithFields(fieldContext.FieldNames());
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q,
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId().ToString(), q,
fieldContext.CancellationToken);
});
@ -176,9 +191,13 @@ internal static class ContentActions
{
var query = fieldContext.BuildODataQuery();
var q = Q.Empty.WithODataQuery(query).WithReference(source.Id).WithoutTotal();
var q = Q.Empty
.WithODataQuery(query)
.WithReference(source.Id)
.WithoutTotal()
.WithFields(fieldContext.FieldNames());
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q,
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId().ToString(), q,
fieldContext.CancellationToken);
});
@ -186,9 +205,12 @@ internal static class ContentActions
{
var query = fieldContext.BuildODataQuery();
var q = Q.Empty.WithODataQuery(query).WithReference(source.Id);
var q = Q.Empty
.WithODataQuery(query)
.WithReference(source.Id)
.WithFields(fieldContext.FieldNames());
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q,
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId().ToString(), q,
fieldContext.CancellationToken);
});
@ -196,9 +218,13 @@ internal static class ContentActions
{
var query = fieldContext.BuildODataQuery();
var q = Q.Empty.WithODataQuery(query).WithReferencing(source.Id).WithoutTotal();
var q = Q.Empty
.WithODataQuery(query)
.WithReferencing(source.Id)
.WithoutTotal()
.WithFields(fieldContext.FieldNames());
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q,
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId().ToString(), q,
fieldContext.CancellationToken);
});
@ -206,9 +232,12 @@ internal static class ContentActions
{
var query = fieldContext.BuildODataQuery();
var q = Q.Empty.WithODataQuery(query).WithReferencing(source.Id);
var q = Q.Empty
.WithODataQuery(query)
.WithReferencing(source.Id)
.WithFields(fieldContext.FieldNames());
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q,
return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId().ToString(), q,
fieldContext.CancellationToken);
});
}

14
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs

@ -11,6 +11,7 @@ using Namotion.Reflection;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure.Json.Objects;
@ -20,20 +21,21 @@ internal static class ContentFields
{
public static readonly IFieldResolver ResolveStringFieldAssets = Resolvers.Async<string, object>(async (value, fieldContext, context) =>
{
var cacheDuration = fieldContext.CacheDuration();
var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedAssetIds(value).ToList();
return await context.GetAssetsAsync(ids, cacheDuration, fieldContext.CancellationToken);
return await context.GetAssetsAsync(ids,
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
});
public static readonly IFieldResolver ResolveStringFieldContents = Resolvers.Async<string, object>(async (value, fieldContext, context) =>
{
var cacheDuration = fieldContext.CacheDuration();
var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedContentIds(value).ToList();
return await context.GetContentsAsync(ids, cacheDuration, fieldContext.CancellationToken);
return await context.GetContentsAsync(ids,
fieldContext.FieldNames(),
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
});
public static readonly FieldType Id = new FieldType

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs

@ -8,16 +8,22 @@
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
internal sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity>
{
// We need the schema identity at runtime.
public DomainId SchemaId { get; set; }
public ContentGraphType(SchemaInfo schemaInfo)
{
// The name is used for equal comparison. Therefore it is important to treat it as readonly.
Name = schemaInfo.ContentType;
SchemaId = schemaInfo.Schema.Id;
}
public void Initialize(Builder builder, SchemaInfo schemaInfo, IEnumerable<SchemaInfo> allSchemas)

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs

@ -15,7 +15,8 @@ internal sealed class ContentUnionGraphType : UnionGraphType
{
private readonly Dictionary<DomainId, IObjectGraphType> types = new Dictionary<DomainId, IObjectGraphType>();
public bool HasType => types.Count > 0;
// We need the schema identity at runtime.
public IReadOnlyDictionary<DomainId, IObjectGraphType> SchemaTypes => types;
public ContentUnionGraphType(Builder builder, string name, ReadonlyList<DomainId>? schemaIds)
{
@ -42,7 +43,7 @@ internal sealed class ContentUnionGraphType : UnionGraphType
}
}
if (HasType)
if (SchemaTypes.Count > 0)
{
foreach (var type in types)
{

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs

@ -74,7 +74,7 @@ internal sealed class DataInputGraphType : InputObjectGraphType
{
if (source.TryGetValue(field.Name, out var value))
{
if (value is IEnumerable<object> list && field.ResolvedType?.Flatten() is IComplexGraphType nestedType)
if (value is IEnumerable<object> list && field.ResolvedType?.InnerType() is IComplexGraphType nestedType)
{
var array = new JsonArray(list.Count());

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs

@ -50,7 +50,7 @@ internal sealed class EmbeddableStringGraphType : ObjectGraphType<string>
{
var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, schemaIds);
if (!union.HasType)
if (union.SchemaTypes.Count == 0)
{
return default;
}

15
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs

@ -100,16 +100,21 @@ internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo>
private static readonly IFieldResolver Assets = CreateAsyncValueResolver((value, fieldContext, context) =>
{
var cacheDuration = fieldContext.CacheDuration();
var ids = value.AsIds();
return context.GetReferencedAssetsAsync(value, cacheDuration, fieldContext.CancellationToken);
return context.GetAssetsAsync(ids,
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
});
private static readonly IFieldResolver References = CreateAsyncValueResolver((value, fieldContext, context) =>
{
var cacheDuration = fieldContext.CacheDuration();
var ids = value.AsIds();
return context.GetReferencedContentsAsync(value, cacheDuration, fieldContext.CancellationToken);
return context.GetContentsAsync(ids,
fieldContext.FieldNames(),
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
});
private readonly Builder builder;
@ -265,7 +270,7 @@ internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo>
{
var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, schemaIds);
if (!union.HasType)
if (union.SchemaTypes.Count == 0)
{
return null;
}

15
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.Types;
using GraphQLParser.AST;
@ -24,4 +25,18 @@ public sealed class CacheDirective : Directive
DefaultValue = 600
});
}
public static TimeSpan CacheDuration(IResolveFieldContext context)
{
var cacheDirective = context.GetDirective("cache");
if (cacheDirective != null)
{
var duration = cacheDirective.GetArgument<int>("duration");
return TimeSpan.FromSeconds(duration);
}
return TimeSpan.Zero;
}
}

26
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/OptimizeFieldQueriesDirective.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.Types;
using GraphQLParser.AST;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives;
public sealed class OptimizeFieldQueriesDirective : Directive
{
public OptimizeFieldQueriesDirective()
: base("optimizeFieldQueries", DirectiveLocation.Field, DirectiveLocation.FragmentSpread, DirectiveLocation.InlineFragment)
{
Description = "Enable Query Optimizations";
}
public static bool IsApplied(IResolveFieldContext context)
{
return context.GetDirective("optimizeFieldQueries") != null;
}
}

13
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs

@ -6,6 +6,7 @@
// ==========================================================================
using GraphQL.Resolvers;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Infrastructure;
using Squidex.Shared.Users;
@ -29,6 +30,16 @@ internal static class EntityResolvers
private static IFieldResolver ResolveUser<TSource>(Func<TSource, RefToken> resolver)
{
return Resolvers.Async<TSource, IUser?>((source, fieldContext, context) => context.FindUserAsync(resolver(source), fieldContext.CancellationToken));
return Resolvers.Async<TSource, IUser?>((source, fieldContext, context) =>
{
var token = resolver(source);
if (fieldContext.HasOnlyIdField())
{
return new ValueTask<IUser?>(new ClientUser(token));
}
return context.FindUserAsync(token, fieldContext.CancellationToken);
});
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs

@ -82,8 +82,8 @@ public static class Resolvers
command.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any);
var commandContext =
await context.Resolve<ICommandBus>()
.PublishAsync(command, fieldContext.CancellationToken);
await context.Resolve<ICommandBus>().PublishAsync(command,
fieldContext.CancellationToken);
return commandContext.PlainResult!;
});

166
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs

@ -8,9 +8,14 @@
using GraphQL;
using GraphQL.Types;
using GraphQL.Utilities;
using GraphQLParser;
using GraphQLParser.AST;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.ObjectPool;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
@ -100,12 +105,12 @@ public static class SharedExtensions
internal static FieldType WithSchemaId(this FieldType field, SchemaInfo value)
{
return field.WithMetadata(nameof(SchemaId), value.Schema.Id.ToString());
return field.WithMetadata(nameof(SchemaId), value.Schema.Id);
}
internal static string SchemaId(this FieldType field)
internal static DomainId SchemaId(this FieldType field)
{
return field.GetMetadata<string>(nameof(SchemaId))!;
return field.GetMetadata<DomainId>(nameof(SchemaId))!;
}
internal static FieldType WithSchemaNamedId(this FieldType field, SchemaInfo value)
@ -118,16 +123,11 @@ public static class SharedExtensions
return field.GetMetadata<NamedId<DomainId>>(nameof(SchemaNamedId))!;
}
internal static IGraphType? Flatten(this QueryArgument type)
{
return type.ResolvedType?.Flatten();
}
public static IGraphType? Flatten(this IGraphType type)
public static IGraphType? InnerType(this IGraphType type)
{
if (type is IProvideResolvedType provider)
{
return provider.ResolvedType?.Flatten();
return provider.ResolvedType?.InnerType();
}
return type;
@ -135,15 +135,151 @@ public static class SharedExtensions
public static TimeSpan CacheDuration(this IResolveFieldContext context)
{
var cacheDirective = context.GetDirective("cache");
return CacheDirective.CacheDuration(context);
}
public static List<DomainId>? AsIds(this JsonValue value)
{
try
{
List<DomainId>? result = null;
if (value.Value is JsonArray a)
{
foreach (var item in a)
{
if (item.Value is string id)
{
result ??= new List<DomainId>();
result.Add(DomainId.Create(id));
}
}
}
return result;
}
catch
{
return null;
}
}
public static bool HasOnlyIdField(this IResolveFieldContext context)
{
return context.FieldAst.SelectionSet?.Selections.TrueForAll(x => x is GraphQLField field && field.Name == "id") == true;
}
public static HashSet<string>? FieldNames(this IResolveFieldContext context)
{
if (!OptimizeFieldQueriesDirective.IsApplied(context))
{
return null;
}
return new FieldNameResolver(context.Document, context.Schema).Iterate(context.FieldAst, context.FieldDefinition.ResolvedType);
}
private sealed class FieldNameResolver
{
private readonly GraphQLDocument document;
private readonly ISchema schema;
private HashSet<string>? fieldNames = new HashSet<string>();
private IComplexGraphType? currentParent;
if (cacheDirective != null)
public FieldNameResolver(GraphQLDocument document, ISchema schema)
{
var duration = cacheDirective.GetArgument<int>("duration");
this.document = document;
this.schema = schema;
}
public HashSet<string>? Iterate(GraphQLField field, IGraphType? type)
{
currentParent = ResolveDataParent(type);
return TimeSpan.FromSeconds(duration);
IterateContent(field.SelectionSet);
return fieldNames;
}
return TimeSpan.Zero;
private void IterateContent(GraphQLSelectionSet? selection)
{
if (selection == null)
{
return;
}
foreach (var selectedField in selection.Selections)
{
switch (selectedField)
{
case GraphQLField field when field.Name == "data":
IterateData(field.SelectionSet);
break;
case GraphQLField field when field.Name == "flatData":
IterateData(field.SelectionSet);
break;
case GraphQLField field when field.Name == "data__dynamic":
fieldNames = null;
return;
case GraphQLFragmentSpread spread:
var fragment = document.FindFragmentDefinition(spread.FragmentName.Name);
IterateContent(fragment?.SelectionSet);
break;
case GraphQLInlineFragment inline when inline.TypeCondition != null:
currentParent = ResolveDataParent(schema.AllTypes.FirstOrDefault(x => x.Name == inline.TypeCondition.Type.Name()));
IterateContent(inline.SelectionSet);
break;
}
}
}
private void IterateData(GraphQLSelectionSet? selection)
{
if (selection == null)
{
return;
}
foreach (var selectedField in selection.Selections)
{
switch (selectedField)
{
case GraphQLField field when currentParent != null:
// The GraphQL field might be different from the schema name. Therefore we need the field type.
var fieldType = currentParent.Fields.Find(field.Name.StringValue);
if (fieldType != null)
{
// Resolve the schema name from the GraphQL field.
fieldNames?.Add(fieldType.SourceName());
}
break;
case GraphQLFragmentSpread spread:
var fragment = document.FindFragmentDefinition(spread.FragmentName.Name);
IterateData(fragment?.SelectionSet);
break;
}
}
}
private static IComplexGraphType? ResolveDataParent(IGraphType? type)
{
if (type?.InnerType() is not IComplexGraphType complexType)
{
return null;
}
// We need to resolve the schema names from the GraphQL field name and the flatData type has this information.
if (complexType?.Fields.Find("flatData")?.ResolvedType?.InnerType() is not IComplexGraphType dataParent)
{
return null;
}
return dataParent;
}
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs

@ -24,7 +24,9 @@ public static class SharedTypes
public static readonly IGraphType EnrichedContentEvent = new EnrichedContentEventGraphType();
public static readonly CacheDirective MemoryCacheDirective = new CacheDirective();
public static readonly CacheDirective CacheDirective = new CacheDirective();
public static readonly OptimizeFieldQueriesDirective OptimizeFieldQueriesDirective = new OptimizeFieldQueriesDirective();
public static readonly FieldType FindAsset = new FieldType
{

1
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -60,6 +60,7 @@ public class ContentQueryParser
WithPaging(query, q);
q = q.WithQuery(query);
q = q.WithFields(context.FieldsList());
if (context.ShouldSkipTotal())
{

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs

@ -35,7 +35,7 @@ internal sealed class GeoQueryTransformer : AsyncTransformVisitor<ClrValue, GeoQ
{
if (nodeIn.Value.Value is FilterSphere sphere)
{
var field = string.Join(".", nodeIn.Path.Skip(1));
var field = string.Join('.', nodeIn.Path.Skip(1));
var searchQuery = new GeoQuery(args.Schema.Id, field, sphere.Latitude, sphere.Longitude, sphere.Radius, 1000);
var searchScope = args.Context.Scope();

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

@ -95,41 +95,35 @@ public abstract class QueryExecutionContext : Dictionary<string, object?>
{
Guard.NotNull(ids);
return await AssetCache.CacheOrQueryAsync(ids, async pendingIds =>
await maxRequests.WaitAsync(ct);
try
{
var q = Q.Empty.WithIds(ids).WithoutTotal();
return await AssetQuery.QueryAsync(Context, null, q, ct);
}
finally
{
await maxRequests.WaitAsync(ct);
try
{
var q = Q.Empty.WithIds(pendingIds).WithoutTotal();
return await AssetQuery.QueryAsync(Context, null, q, ct);
}
finally
{
maxRequests.Release();
}
});
maxRequests.Release();
}
}
public virtual async Task<IReadOnlyList<IEnrichedContentEntity>> QueryContentsByIdsAsync(IEnumerable<DomainId> ids,
public virtual async Task<IReadOnlyList<IEnrichedContentEntity>> QueryContentsByIdsAsync(IEnumerable<DomainId> ids, HashSet<string>? fields,
CancellationToken ct)
{
Guard.NotNull(ids);
return await ContentCache.CacheOrQueryAsync(ids, async pendingIds =>
await maxRequests.WaitAsync(ct);
try
{
var q = Q.Empty.WithIds(ids).WithFields(fields).WithoutTotal();
return await ContentQuery.QueryAsync(Context, q, ct);
}
finally
{
await maxRequests.WaitAsync(ct);
try
{
var q = Q.Empty.WithIds(pendingIds).WithoutTotal();
return await ContentQuery.QueryAsync(Context, q, ct);
}
finally
{
maxRequests.Release();
}
});
maxRequests.Release();
}
}
public T Resolve<T>() where T : class

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs

@ -136,7 +136,7 @@ public sealed class ConvertData : IContentEnricherStep
converter.Add(
new ResolveLanguages(
context.App.Languages,
context.LanguageList().ToArray())
context.LanguagesList().ToArray())
{
ResolveFallback = !context.IsFrontendClient && context.ShouldResolveLanguages()
});

6
backend/src/Squidex.Domain.Apps.Entities/ContextHeaders.cs

@ -9,10 +9,10 @@ namespace Squidex.Domain.Apps.Entities;
public static class ContextHeaders
{
public const string NoTotal = "X-NoTotal";
public const string NoSlowTotal = "X-NoSlowTotal";
public const string NoCacheKeys = "X-NoCacheKeys";
public const string NoScripting = "X-NoScripting";
public const string NoSlowTotal = "X-NoSlowTotal";
public const string NoTotal = "X-NoTotal";
public static bool ShouldSkipCacheKeys(this Context context)
{
@ -72,7 +72,7 @@ public static class ContextHeaders
{
if (values?.Any() == true)
{
builder.SetHeader(key, string.Join(",", values));
builder.SetHeader(key, string.Join(',', values));
}
else
{

2
backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs

@ -51,4 +51,6 @@ public interface IAppProvider
Task<IRuleEntity?> GetRuleAsync(DomainId appId, DomainId id,
CancellationToken ct = default);
void RegisterAppForLocalContext(DomainId appId, IAppEntity app);
}

23
backend/src/Squidex.Domain.Apps.Entities/Q.cs

@ -20,6 +20,8 @@ public record Q
public IReadOnlyList<DomainId>? Ids { get; init; }
public IReadOnlySet<string>? Fields { get; init; }
public DomainId Referencing { get; init; }
public DomainId Reference { get; init; }
@ -91,16 +93,31 @@ public record Q
return this with { Ids = ids?.ToList() };
}
public Q WithIds(IEnumerable<DomainId> ids)
public Q WithIds(IEnumerable<DomainId>? ids)
{
return this with { Ids = ids?.ToList() };
}
public Q WithFields(params string[] fields)
{
return this with { Fields = fields?.ToHashSet() };
}
public Q WithFields(IEnumerable<string>? fields)
{
return this with { Fields = fields?.ToHashSet() };
}
public Q WithSchedule(Instant from, Instant to)
{
return this with { ScheduledFrom = from, ScheduledTo = to };
}
public Q WithFields(string? fields)
{
return this with { Fields = fields?.Split(',', StringSplitOptions.RemoveEmptyEntries).ToHashSet() };
}
public Q WithIds(string? ids)
{
if (string.IsNullOrWhiteSpace(ids))
@ -110,9 +127,9 @@ public record Q
var idsList = new List<DomainId>();
if (!string.IsNullOrEmpty(ids))
if (ids.Length > 0)
{
foreach (var id in ids.Split(','))
foreach (var id in ids.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
idsList.Add(DomainId.Create(id));
}

4
backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -628,7 +628,7 @@ public sealed class MongoUserStore :
public Task ReplaceCodesAsync(IdentityUser user, IEnumerable<string> recoveryCodes,
CancellationToken cancellationToken)
{
((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes));
((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(';', recoveryCodes));
return Task.CompletedTask;
}
@ -643,7 +643,7 @@ public sealed class MongoUserStore :
{
var updatedCodes = new List<string>(splitCodes.Where(s => s != code));
((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes));
((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(';', updatedCodes));
return Task.FromResult(true);
}

2
backend/src/Squidex.Domain.Users/UserWithClaims.cs

@ -30,7 +30,7 @@ internal sealed class UserWithClaims : IUser
public bool IsLocked
{
get => snapshot.LockoutEnd > DateTime.UtcNow;
get => snapshot.LockoutEnd > DateTimeOffset.UtcNow;
}
public IReadOnlyList<Claim> Claims { get; }

1
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs

@ -14,7 +14,6 @@ namespace Squidex.Infrastructure.EventSourcing;
public partial class MongoEventStore
{
private const int MaxCommitSize = 100;
private const int MaxWriteAttempts = 20;
private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0);

18
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs

@ -71,35 +71,35 @@ public static class MongoExtensions
}
public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find,
Expression<Func<T, object>> include)
Expression<Func<T, object?>> include)
{
return find.Project<BsonDocument>(Builders<T>.Projection.Include(include));
}
public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find,
Expression<Func<T, object>> include1,
Expression<Func<T, object>> include2)
Expression<Func<T, object?>> include1,
Expression<Func<T, object?>> include2)
{
return find.Project<BsonDocument>(Builders<T>.Projection.Include(include1).Include(include2));
}
public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find,
Expression<Func<T, object>> include1,
Expression<Func<T, object>> include2,
Expression<Func<T, object>> include3)
Expression<Func<T, object?>> include1,
Expression<Func<T, object?>> include2,
Expression<Func<T, object?>> include3)
{
return find.Project<BsonDocument>(Builders<T>.Projection.Include(include1).Include(include2).Include(include3));
}
public static IFindFluent<T, T> Not<T>(this IFindFluent<T, T> find,
Expression<Func<T, object>> exclude)
Expression<Func<T, object?>> exclude)
{
return find.Project<T>(Builders<T>.Projection.Exclude(exclude));
}
public static IFindFluent<T, T> Not<T>(this IFindFluent<T, T> find,
Expression<Func<T, object>> exclude1,
Expression<Func<T, object>> exclude2)
Expression<Func<T, object?>> exclude1,
Expression<Func<T, object?>> exclude2)
{
return find.Project<T>(Builders<T>.Projection.Exclude(exclude1).Exclude(exclude2));
}

4
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/ProfilerCollection.cs

@ -16,11 +16,9 @@ public sealed class ProfilerCollection
{
private readonly IMongoCollection<ProfilerDocument> collection;
public string CollectionName => "system.profile";
public ProfilerCollection(IMongoDatabase database)
{
collection = database.GetCollection<ProfilerDocument>(CollectionName);
collection = database.GetCollection<ProfilerDocument>("system.profile");
}
public async Task<IReadOnlyList<ProfilerDocument>> GetQueriesAsync(string collectionName,

2
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs

@ -33,7 +33,7 @@ public static class SortBuilder
public static SortDefinition<T> OrderBy<T>(SortNode sort)
{
var propertyName = string.Join(".", sort.Path);
var propertyName = string.Join('.', sort.Path);
if (sort.Order == SortOrder.Ascending)
{

2
backend/src/Squidex.Infrastructure/Commands/DomainObject.cs

@ -344,7 +344,7 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
}
}
public async Task RebuildStateAsync(
public async virtual Task RebuildStateAsync(
CancellationToken ct = default)
{
await EnsureLoadedAsync(ct);

14
backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.Threading.Tasks.Dataflow;
using Google.Api;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Squidex.Caching;
@ -37,6 +38,19 @@ public class Rebuilder
this.log = log;
}
public virtual async Task<T> RebuildStateAsync<T, TState>(DomainId id,
CancellationToken ct = default)
where T : DomainObject<TState> where TState : class, IDomainState<TState>, new()
{
var store = serviceProvider.GetRequiredService<IStore<TState>>();
var domainObject = domainObjectFactory.Create<T, TState>(id, store);
await domainObject.EnsureLoadedAsync(ct);
return domainObject;
}
public virtual Task RebuildAsync<T, TState>(string filter, int batchSize,
CancellationToken ct = default)
where T : DomainObject<TState> where TState : class, IDomainState<TState>, new()

2
backend/src/Squidex.Infrastructure/Json/System/Constants.cs

@ -9,7 +9,7 @@ using System.Text.Json;
namespace Squidex.Infrastructure.Json.System;
internal sealed class Constants
internal static class Constants
{
public const string DefaultDiscriminatorProperty = "$type";

6
backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs

@ -63,7 +63,7 @@ public static class EdmModelConverter
entityPath.Push(fieldName);
var typeName = string.Join("_", entityPath.Reverse().Select(x => x.EscapeEdmField().ToPascalCase()));
var typeName = string.Join('_', entityPath.Reverse().Select(x => x.EscapeEdmField().ToPascalCase()));
var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == typeName);
@ -114,11 +114,11 @@ public static class EdmModelConverter
public static string EscapeEdmField(this string field)
{
return field.Replace("-", "_", StringComparison.Ordinal);
return field.Replace('-', '_');
}
public static string UnescapeEdmField(this string field)
{
return field.Replace("_", "-", StringComparison.Ordinal);
return field.Replace('_', '-');
}
}

2
backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs

@ -90,7 +90,7 @@ public sealed class PropertyPath : ReadonlyList<string>, IEquatable<string>
public override string ToString()
{
return string.Join(".", this);
return string.Join('.', this);
}
private static PropertyPath Create(IEnumerable<string>? source)

2
backend/src/Squidex.Infrastructure/Queries/Query.cs

@ -58,7 +58,7 @@ public class Query<TValue>
if (FullText != null)
{
sb.AppendIfNotEmpty("; ");
sb.Append($"FullText: '{FullText.Replace("'", "\'", StringComparison.Ordinal)}'");
sb.Append($"FullText: '{FullText.Replace('\'', '\'')}'");
}
if (Skip > 0)

2
backend/src/Squidex.Infrastructure/Queries/SortNode.cs

@ -13,7 +13,7 @@ public sealed record SortNode(PropertyPath Path, SortOrder Order)
{
public override string ToString()
{
var path = string.Join(".", Path);
var path = string.Join('.', Path);
return $"{path} {Order}";
}

6
backend/src/Squidex.Infrastructure/RandomHash.cs

@ -17,9 +17,9 @@ public static class RandomHash
return Guid.NewGuid()
.ToString().ToSha256Base64()
.ToLowerInvariant()
.Replace("+", "x", StringComparison.Ordinal)
.Replace("=", "x", StringComparison.Ordinal)
.Replace("/", "x", StringComparison.Ordinal);
.Replace('+', 'x')
.Replace('=', 'x')
.Replace('/', 'x');
}
public static string Simple()

2
backend/src/Squidex.Infrastructure/Security/PermissionSet.cs

@ -38,7 +38,7 @@ public sealed class PermissionSet : ReadonlyList<Permission>
public PermissionSet(IList<Permission> permissions)
: base(permissions)
{
display = new Lazy<string>(() => string.Join(";", this));
display = new Lazy<string>(() => string.Join(';', this));
}
public PermissionSet Add(string permission)

5
backend/src/Squidex.Web/ContextProvider.cs

@ -21,10 +21,7 @@ public sealed class ContextProvider : IContextProvider
{
if (httpContextAccessor.HttpContext == null)
{
if (asyncLocal.Value == null)
{
asyncLocal.Value = Context.Anonymous(null!);
}
asyncLocal.Value ??= Context.Anonymous(null!);
return asyncLocal.Value;
}

6
backend/src/Squidex.Web/Pipeline/JsonStreamResult.cs

@ -57,9 +57,7 @@ public sealed class JsonStreamResult<T> : ActionResult
private static void DisableResponseBuffering(HttpContext context)
{
var bufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
if (bufferingFeature != null)
{
bufferingFeature.DisableBuffering();
}
bufferingFeature?.DisableBuffering();
}
}

101
backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeader.cs

@ -0,0 +1,101 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NSwag;
using NSwag.Annotations;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
#pragma warning disable MA0048 // File name must match type name
namespace Squidex.Areas.Api.Config.OpenApi;
public class AcceptHeader
{
public sealed class UnpublishedAttribute : BaseAttribute
{
public UnpublishedAttribute()
: base(ContentHeaders.Unpublished, "Return unpublished content items.", JsonObjectType.Boolean)
{
}
}
public sealed class FieldsAttribute : BaseAttribute
{
public FieldsAttribute()
: base(ContentHeaders.Fields, "The list of content fields (comma-separated).", JsonObjectType.String)
{
}
}
public sealed class FlattenAttribute : BaseAttribute
{
public FlattenAttribute()
: base(ContentHeaders.Flatten, "Provide the data as flat object.", JsonObjectType.Boolean)
{
}
}
public sealed class LanguagesAttribute : BaseAttribute
{
public LanguagesAttribute()
: base(ContentHeaders.Languages, "The list of languages to resolve (comma-separated).")
{
}
}
public sealed class NoTotalAttribute : BaseAttribute
{
public NoTotalAttribute()
: base(ContextHeaders.NoTotal, "Do not return the total amount.", JsonObjectType.Boolean)
{
}
}
public sealed class NoSlowTotalAttribute : BaseAttribute
{
public NoSlowTotalAttribute()
: base(ContextHeaders.NoSlowTotal, "Do not return the total amount, if it would be slow.", JsonObjectType.Boolean)
{
}
}
public abstract class BaseAttribute : OpenApiOperationProcessorAttribute
{
protected BaseAttribute(string name, string description, JsonObjectType schemaType = JsonObjectType.String)
: base(typeof(Processor), name, description, schemaType)
{
}
#pragma warning disable IDE1006 // Naming Styles
public record Processor(string name, string description, JsonObjectType schemaType) : IOperationProcessor
#pragma warning restore IDE1006 // Naming Styles
{
public bool Process(OperationProcessorContext context)
{
var parameter = new OpenApiParameter
{
Name = name,
Kind = OpenApiParameterKind.Header,
Schema = new JsonSchema
{
Type = schemaType
},
Description = description
};
context.OperationDescription.Operation.Parameters.Add(parameter);
context.OperationDescription.Operation.SetPositions();
return true;
}
}
}
}

99
backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptHeaderAttribute.cs

@ -1,99 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NSwag;
using NSwag.Annotations;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
#pragma warning disable MA0048 // File name must match type name
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class AcceptHeader_Unpublished : AcceptHeaderAttribute
{
public AcceptHeader_Unpublished()
: base(ContentHeaders.Unpublished, "Return unpublished content items.", JsonObjectType.Boolean)
{
}
}
public sealed class AcceptHeader_Flatten : AcceptHeaderAttribute
{
public AcceptHeader_Flatten()
: base(ContentHeaders.Flatten, "Provide the data as flat object.", JsonObjectType.Boolean)
{
}
}
public sealed class AcceptHeader_Languages : AcceptHeaderAttribute
{
public AcceptHeader_Languages()
: base(ContentHeaders.Languages, "Only resolve these languages (comma-separated).")
{
}
}
public sealed class AcceptHeader_NoTotal : AcceptHeaderAttribute
{
public AcceptHeader_NoTotal()
: base(ContextHeaders.NoTotal, "Do not return the total amount.", JsonObjectType.Boolean)
{
}
}
public sealed class AcceptHeader_NoSlowTotal : AcceptHeaderAttribute
{
public AcceptHeader_NoSlowTotal()
: base(ContextHeaders.NoSlowTotal, "Do not return the total amount, if it would be slow.", JsonObjectType.Boolean)
{
}
}
public class AcceptHeaderAttribute : OpenApiOperationProcessorAttribute
{
public AcceptHeaderAttribute(string name, string description, JsonObjectType schemaType = JsonObjectType.String)
: base(typeof(Processor), name, description, schemaType)
{
}
public sealed class Processor : IOperationProcessor
{
private readonly string name;
private readonly string description;
private readonly JsonObjectType schemaType;
public Processor(string name, string description, JsonObjectType schemaType)
{
this.name = name;
this.description = description;
this.schemaType = schemaType;
}
public bool Process(OperationProcessorContext context)
{
var parameter = new OpenApiParameter
{
Name = name,
Kind = OpenApiParameterKind.Header,
Schema = new JsonSchema
{
Type = schemaType
},
Description = description
};
context.OperationDescription.Operation.Parameters.Add(parameter);
context.OperationDescription.Operation.SetPositions();
return true;
}
}
}

8
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -115,8 +115,8 @@ public sealed class AssetsController : ApiController
[ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)]
[ApiCosts(1)]
[AcceptQuery(false)]
[AcceptHeader_NoTotal]
[AcceptHeader_NoSlowTotal]
[AcceptHeader.NoTotal]
[AcceptHeader.NoSlowTotal]
public async Task<IActionResult> GetAssets(string app, [FromQuery] DomainId? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null)
{
var assets = await assetQuery.QueryAsync(Context, parentId, CreateQuery(ids, q), HttpContext.RequestAborted);
@ -144,8 +144,8 @@ public sealed class AssetsController : ApiController
[ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)]
[ApiCosts(1)]
[AcceptHeader_NoTotal]
[AcceptHeader_NoSlowTotal]
[AcceptHeader.NoTotal]
[AcceptHeader.NoSlowTotal]
public async Task<IActionResult> GetAssetsPost(string app, [FromBody] QueryDto query)
{
var assets = await assetQuery.QueryAsync(Context, query?.ParentId, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);

87
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -79,11 +79,12 @@ public sealed class ContentsController : ApiController
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[AcceptQuery(true)]
[AcceptHeader_Flatten]
[AcceptHeader_Languages]
[AcceptHeader_NoSlowTotal]
[AcceptHeader_NoTotal]
[AcceptHeader_Unpublished]
[AcceptHeader.Fields]
[AcceptHeader.Flatten]
[AcceptHeader.Languages]
[AcceptHeader.NoSlowTotal]
[AcceptHeader.NoTotal]
[AcceptHeader.Unpublished]
public async Task<IActionResult> GetContents(string app, string schema, [FromQuery] string? ids = null, [FromQuery] string? q = null)
{
var contents = await contentQuery.QueryAsync(Context, schema, CreateQuery(ids, q), HttpContext.RequestAborted);
@ -112,11 +113,12 @@ public sealed class ContentsController : ApiController
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[AcceptHeader_Flatten]
[AcceptHeader_Languages]
[AcceptHeader_NoSlowTotal]
[AcceptHeader_NoTotal]
[AcceptHeader_Unpublished]
[AcceptHeader.Fields]
[AcceptHeader.Flatten]
[AcceptHeader.Languages]
[AcceptHeader.NoSlowTotal]
[AcceptHeader.NoTotal]
[AcceptHeader.Unpublished]
public async Task<IActionResult> GetContentsPost(string app, string schema, [FromBody] QueryDto query)
{
var contents = await contentQuery.QueryAsync(Context, schema, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);
@ -146,9 +148,10 @@ public sealed class ContentsController : ApiController
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[AcceptHeader_Flatten]
[AcceptHeader_Languages]
[AcceptHeader_Unpublished]
[AcceptHeader.Fields]
[AcceptHeader.Flatten]
[AcceptHeader.Languages]
[AcceptHeader.Unpublished]
public async Task<IActionResult> GetContent(string app, string schema, DomainId id, long version = EtagVersion.Any)
{
var content = await contentQuery.FindAsync(Context, schema, id, version, HttpContext.RequestAborted);
@ -209,11 +212,12 @@ public sealed class ContentsController : ApiController
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[AcceptHeader_Flatten]
[AcceptHeader_Languages]
[AcceptHeader_Unpublished]
[AcceptHeader_NoSlowTotal]
[AcceptHeader_NoTotal]
[AcceptHeader.Fields]
[AcceptHeader.Flatten]
[AcceptHeader.Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.NoSlowTotal]
[AcceptHeader.NoTotal]
public async Task<IActionResult> GetReferences(string app, string schema, DomainId id, [FromQuery] string? q = null)
{
var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id), HttpContext.RequestAborted);
@ -243,11 +247,12 @@ public sealed class ContentsController : ApiController
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[AcceptHeader_Flatten]
[AcceptHeader_Languages]
[AcceptHeader_Unpublished]
[AcceptHeader_NoSlowTotal]
[AcceptHeader_NoTotal]
[AcceptHeader.Fields]
[AcceptHeader.Flatten]
[AcceptHeader.Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.NoSlowTotal]
[AcceptHeader.NoTotal]
public async Task<IActionResult> GetReferencing(string app, string schema, DomainId id, [FromQuery] string? q = null)
{
var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id), HttpContext.RequestAborted);
@ -276,8 +281,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/{version}/")]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)]
[ApiCosts(1)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[Obsolete("Use ID endpoint with version query.")]
public async Task<IActionResult> GetContentVersion(string app, string schema, DomainId id, int version)
{
@ -312,8 +317,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status201Created)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsCreate)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string schema, CreateContentDto request)
{
@ -400,8 +405,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsUpsert)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> PostUpsertContent(string app, string schema, DomainId id, UpsertContentDto request)
{
@ -429,8 +434,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string schema, DomainId id, [FromBody] ContentData request)
{
@ -458,8 +463,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string schema, DomainId id, [FromBody] ContentData request)
{
@ -487,8 +492,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/status/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string schema, DomainId id, [FromBody] ChangeStatusDto request)
{
@ -515,8 +520,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/status/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContentStatus(string app, string schema, DomainId id)
{
@ -542,8 +547,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/draft/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionCreateOwn)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> CreateDraft(string app, string schema, DomainId id)
{
@ -569,8 +574,8 @@ public sealed class ContentsController : ApiController
[Route("content/{app}/{schema}/{id}/draft/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionDeleteOwn)]
[AcceptHeader_Unpublished]
[AcceptHeader_Languages]
[AcceptHeader.Unpublished]
[AcceptHeader.Languages]
[ApiCosts(1)]
public async Task<IActionResult> DeleteVersion(string app, string schema, DomainId id)
{

24
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs

@ -48,7 +48,7 @@ public sealed class ContentsSharedController : ApiController
[Route("content/{app}/graphql/batch")]
[ApiPermissionOrAnonymous]
[ApiCosts(2)]
[AcceptHeader_Unpublished]
[AcceptHeader.Unpublished]
[IgnoreCacheFilter]
public IActionResult GetGraphQL(string app)
{
@ -75,11 +75,12 @@ public sealed class ContentsSharedController : ApiController
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[AcceptHeader_Flatten]
[AcceptHeader_Languages]
[AcceptHeader_NoSlowTotal]
[AcceptHeader_NoTotal]
[AcceptHeader_Unpublished]
[AcceptHeader.Fields]
[AcceptHeader.Flatten]
[AcceptHeader.Languages]
[AcceptHeader.NoSlowTotal]
[AcceptHeader.NoTotal]
[AcceptHeader.Unpublished]
public async Task<IActionResult> GetAllContents(string app, AllContentsByGetDto query)
{
var contents = await contentQuery.QueryAsync(Context, (query ?? new AllContentsByGetDto()).ToQuery(Request), HttpContext.RequestAborted);
@ -107,11 +108,12 @@ public sealed class ContentsSharedController : ApiController
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[AcceptHeader_Flatten]
[AcceptHeader_Languages]
[AcceptHeader_NoSlowTotal]
[AcceptHeader_NoTotal]
[AcceptHeader_Unpublished]
[AcceptHeader.Fields]
[AcceptHeader.Flatten]
[AcceptHeader.Languages]
[AcceptHeader.NoSlowTotal]
[AcceptHeader.NoTotal]
[AcceptHeader.Unpublished]
public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] AllContentsByPostDto query)
{
var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);

2
backend/src/Squidex/Areas/IdentityServer/Config/AlwaysAddScopeHandler.cs

@ -25,7 +25,7 @@ public sealed class AlwaysAddScopeHandler : IOpenIddictServerHandler<ProcessSign
{
var scopes = context.AccessTokenPrincipal?.GetScopes() ?? ImmutableArray<string>.Empty;
context.Response.Scope = string.Join(" ", scopes);
context.Response.Scope = string.Join(' ', scopes);
}
return default;

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

@ -51,9 +51,6 @@ public static class BackupsServices
services.AddTransientAs<BackupSchemas>()
.As<IBackupHandler>();
services.AddTransientAs<DefaultBackupHandlerFactory>()
.As<IBackupHandlerFactory>();
services.AddTransientAs<RestoreProcessor>()
.AsSelf();
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -293,6 +293,6 @@ public class RuleEventFormatterTests
Assert.Equal("{'categories':['ref1','ref2','ref3']}", actual?
.Replace(" ", string.Empty, StringComparison.Ordinal)
.Replace("\"", "'", StringComparison.Ordinal));
.Replace('"', '\''));
}
}

36
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Squidex.Assets;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Backup;
@ -25,11 +24,12 @@ public class BackupAppsTests : GivenContext
private readonly IAppsIndex appsIndex = A.Fake<IAppsIndex>();
private readonly IAppUISettings appUISettings = A.Fake<IAppUISettings>();
private readonly IAppImageStore appImageStore = A.Fake<IAppImageStore>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly BackupApps sut;
public BackupAppsTests()
{
sut = new BackupApps(rebuilder, appImageStore, appsIndex, appUISettings);
sut = new BackupApps(rebuilder, appImageStore, appProvider, appsIndex, appUISettings);
}
[Fact]
@ -58,8 +58,17 @@ public class BackupAppsTests : GivenContext
[Fact]
public async Task Should_complete_reservation_with_previous_token()
{
var appObject = A.Fake<AppDomainObject>();
var appState = new AppDomainObject.State();
var context = CreateRestoreContext();
A.CallTo(() => appObject.Snapshot)
.Returns(appState);
A.CallTo(() => rebuilder.RebuildStateAsync<AppDomainObject, AppDomainObject.State>(context.AppId, CancellationToken))
.Returns(appObject);
A.CallTo(() => appsIndex.ReserveAsync(AppId.Id, AppId.Name, CancellationToken))
.Returns("Reservation");
@ -68,12 +77,13 @@ public class BackupAppsTests : GivenContext
Name = AppId.Name
}), context, CancellationToken);
await sut.RestoreAsync(context, CancellationToken);
await sut.CompleteRestoreAsync(context, AppId.Name);
A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default))
.MustHaveHappened();
A.CallTo(() => rebuilder.InsertManyAsync<AppDomainObject, AppDomainObject.State>(A<IEnumerable<DomainId>>.That.Is(AppId.Id), 1, default))
A.CallTo(() => appObject.RebuildStateAsync(default))
.MustHaveHappened();
}
@ -137,6 +147,26 @@ public class BackupAppsTests : GivenContext
.MustHaveHappened();
}
[Fact]
public async Task Should_register_app_to_provider()
{
var appObject = A.Fake<AppDomainObject>();
var appState = new AppDomainObject.State();
var context = CreateRestoreContext();
A.CallTo(() => appObject.Snapshot)
.Returns(appState);
A.CallTo(() => rebuilder.RebuildStateAsync<AppDomainObject, AppDomainObject.State>(context.AppId, CancellationToken))
.Returns(appObject);
await sut.RestoreAsync(context, CancellationToken);
A.CallTo(() => appProvider.RegisterAppForLocalContext(context.AppId, appObject.Snapshot))
.MustHaveHappened();
}
[Fact]
public async Task Should_read_user_settings()
{

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs

@ -120,7 +120,7 @@ public class AssetCommandMiddlewareTests : HandlerTestBase<AssetDomainObject.Sta
{
var actual = CreateAsset();
SetupSameHashAsset(file.FileName, file.FileSize, out var unused);
SetupSameHashAsset(file.FileName, file.FileSize, out var _);
var context =
await HandleAsync(new CreateAsset { File = file, Duplicate = true },
@ -174,7 +174,7 @@ public class AssetCommandMiddlewareTests : HandlerTestBase<AssetDomainObject.Sta
{
var actual = CreateAsset();
SetupSameHashAsset(file.FileName, file.FileSize, out var unused);
SetupSameHashAsset(file.FileName, file.FileSize, out var _);
var context =
await HandleAsync(new UpsertAsset { File = file },

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/EnrichForCachingTests.cs

@ -34,13 +34,14 @@ public class EnrichForCachingTests : GivenContext
Assert.Equal(new List<string>
{
"X-Fields",
"X-Flatten",
"X-Languages",
"X-NoCleanup",
"X-NoEnrichment",
"X-NoResolveLanguages",
"X-ResolveFlow",
"X-Resolve-Urls",
"X-ResolveUrls",
"X-Unpublished"
}, headers);
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs

@ -301,6 +301,6 @@ public class GraphQLIntrospectionTests : GraphQLTestBase
{
var type = (IObjectGraphType)graphQLSchema.AllTypes.Single(x => x.Name == schema);
return (IObjectGraphType?)type.GetField("flatData")?.ResolvedType?.Flatten();
return (IObjectGraphType?)type.GetField("flatData")?.ResolvedType?.InnerType();
}
}

157
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -246,7 +246,8 @@ public class GraphQLQueriesTests : GraphQLTestBase
var query = CreateQuery(@"
query {
findAsset(id: '<ID>') {
id
id,
version
}
}", assetId);
@ -416,7 +417,8 @@ public class GraphQLQueriesTests : GraphQLTestBase
var query = CreateQuery(@"
query {
findMySchemaContent(id: '<ID>') {
id
id,
version
}
}", contentId);
@ -447,7 +449,8 @@ public class GraphQLQueriesTests : GraphQLTestBase
var query = CreateQuery(@"
query {
findMySchemaContent(id: '<ID>') {
id
id,
version
}
}", contentId);
@ -1291,4 +1294,152 @@ public class GraphQLQueriesTests : GraphQLTestBase
Assert.Contains("\"errors\"", json, StringComparison.Ordinal);
}
[Fact]
public async Task Should_query_only_selected_fields()
{
var query = CreateQuery(@"
query {
queryMySchemaContents @optimizeFieldQueries {
data {
myNumber {
iv
}
}
}
}");
await ExecuteAsync(new ExecutionOptions { Query = query });
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(),
A<Q>.That.HasFields(new[] { "my-number" }),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_query_only_selected_flat_fields()
{
var query = CreateQuery(@"
query {
queryMySchemaContents @optimizeFieldQueries {
flatData {
myNumber
}
}
}");
await ExecuteAsync(new ExecutionOptions { Query = query });
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(),
A<Q>.That.HasFields(new[] { "my-number" }),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_query_all_fields_when_directive_not_applied()
{
var query = CreateQuery(@"
query {
queryMySchemaContents {
data {
myNumber {
iv
}
}
}
}");
await ExecuteAsync(new ExecutionOptions { Query = query });
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(),
A<Q>.That.Matches(x => x.Fields == null),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_query_all_fields_when_dynamic_data_is_queried()
{
var query = CreateQuery(@"
query {
queryMySchemaContents @optimizeFieldQueries {
flatData {
myNumber
}
data__dynamic
}
}");
await ExecuteAsync(new ExecutionOptions { Query = query });
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(),
A<Q>.That.Matches(x => x.Fields == null),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_query_all_fields_across_schemas()
{
var query = CreateQuery(@"
query {
queryContentsByIds(ids: [""42""]) @optimizeFieldQueries {
...on MySchema {
flatData {
myNumber
}
}
}
}");
await ExecuteAsync(new ExecutionOptions { Query = query });
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasFields(new[] { "my-number" }),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_fetch_user_if_only_is_id_queried()
{
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId);
var query = CreateQuery(@"
query {
findMySchemaContent(id: '<ID>') {
createdByUser {
id
}
}
}", contentId);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(1, content));
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
var expected = new
{
data = new
{
findMySchemaContent = new
{
createdByUser = new
{
id = content.CreatedBy.Identifier
}
}
}
};
AssertResult(expected, actual);
A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
}

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -167,8 +167,8 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
protected static string CreateQuery(string query, DomainId id = default, IEnrichedContentEntity? content = null)
{
query = query
.Replace("'", "\"", StringComparison.Ordinal)
.Replace("`", "\"", StringComparison.Ordinal)
.Replace('\'', '"')
.Replace('`', '"')
.Replace("<FIELDS_ASSET>", TestAsset.AllFields, StringComparison.Ordinal)
.Replace("<FIELDS_CONTENT>", TestContent.AllFields, StringComparison.Ordinal)
.Replace("<FIELDS_CONTENT_FLAT>", TestContent.AllFlatFields, StringComparison.Ordinal);
@ -202,7 +202,7 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
return A<Context>.That.Matches(x =>
x.App == TestApp.Default &&
x.ShouldSkipCleanup() &&
x.ShouldSkipContentEnrichment() &&
x.ShouldSkipAssetEnrichment() &&
x.UserPrincipal == requestContext.UserPrincipal);
}

41
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs

@ -9,22 +9,21 @@ using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.DomainObject;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb;
public class ContentMappingTests
public class ContentMappingTests : GivenContext
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
[Fact]
public async Task Should_map_content_without_new_version_to_draft()
{
var source = CreateContentWithoutNewVersion();
var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version);
var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider);
var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, AppProvider, default);
Assert.Equal(source.CurrentVersion.Data, snapshot.Data);
Assert.Null(snapshot.DraftData);
@ -43,7 +42,7 @@ public class ContentMappingTests
var source = CreateContentWithoutNewVersion();
var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version);
var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, appProvider);
var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, AppProvider, default);
Assert.Equal(source.CurrentVersion.Data, snapshot.Data);
Assert.Null(snapshot.DraftData);
@ -58,7 +57,7 @@ public class ContentMappingTests
var source = CreateContentWithNewVersion();
var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version);
var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider);
var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, AppProvider, default);
Assert.Equal(source.NewVersion?.Data, snapshot.Data);
Assert.Equal(source.CurrentVersion.Data, snapshot.DraftData);
@ -77,7 +76,7 @@ public class ContentMappingTests
var source = CreateContentWithNewVersion();
var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version);
var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, appProvider);
var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, AppProvider, default);
Assert.Equal(source.CurrentVersion?.Data, snapshot.Data);
Assert.Null(snapshot.DraftData);
@ -86,10 +85,8 @@ public class ContentMappingTests
Assert.False(snapshot.IsSnapshot);
}
private static ContentDomainObject.State CreateContentWithoutNewVersion()
private ContentDomainObject.State CreateContentWithoutNewVersion()
{
var user = RefToken.User("1");
var data =
new ContentData()
.AddField("my-field",
@ -101,25 +98,23 @@ public class ContentMappingTests
var state = new ContentDomainObject.State
{
Id = DomainId.NewGuid(),
AppId = NamedId.Of(DomainId.NewGuid(), "my-app"),
AppId = AppId,
Created = time,
CreatedBy = user,
CreatedBy = User,
CurrentVersion = new ContentVersion(Status.Archived, data),
IsDeleted = true,
LastModified = time,
LastModifiedBy = user,
ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, user, time),
SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"),
LastModifiedBy = User,
ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, User, time),
SchemaId = SchemaId,
Version = 42
};
return state;
}
private static ContentDomainObject.State CreateContentWithNewVersion()
private ContentDomainObject.State CreateContentWithNewVersion()
{
var user = RefToken.User("1");
var data =
new ContentData()
.AddField("my-field",
@ -137,16 +132,16 @@ public class ContentMappingTests
var state = new ContentDomainObject.State
{
Id = DomainId.NewGuid(),
AppId = NamedId.Of(DomainId.NewGuid(), "my-app"),
AppId = AppId,
Created = time,
CreatedBy = user,
CreatedBy = User,
CurrentVersion = new ContentVersion(Status.Archived, data),
IsDeleted = true,
LastModified = time,
LastModifiedBy = user,
LastModifiedBy = User,
NewVersion = new ContentVersion(Status.Published, newData),
ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, user, time),
SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"),
ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, User, time),
SchemaId = SchemaId,
Version = 42
};

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs

@ -35,13 +35,14 @@ public class EnrichForCachingTests : GivenContext
Assert.Equal(new List<string>
{
"X-Fields",
"X-Flatten",
"X-Languages",
"X-NoCleanup",
"X-NoEnrichment",
"X-NoResolveLanguages",
"X-ResolveFlow",
"X-Resolve-Urls",
"X-ResolveUrls",
"X-Unpublished"
}, headers);
}

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs

@ -27,6 +27,11 @@ public static class AExtensions
return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids));
}
public static Q HasFields(this INegatableArgumentConstraintManager<Q> that, IEnumerable<string> fields)
{
return that.Matches(x => x.Fields != null && x.Fields.SetEquals(fields.ToHashSet()));
}
public static Q HasIds(this INegatableArgumentConstraintManager<Q> that, IEnumerable<DomainId> ids)
{
return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids.ToHashSet()));

2
backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorIntegrationTests.cs

@ -115,10 +115,12 @@ public abstract class EventConsumerProcessorIntegrationTests
for (var j = 0; j < numEvents; j++)
{
#pragma warning disable MA0040 // Forward the CancellationToken parameter to methods that take one
await persistence.WriteEventsAsync(new List<Envelope<IEvent>>
{
Envelope.Create(new MyEvent())
});
#pragma warning restore MA0040 // Forward the CancellationToken parameter to methods that take one
}
});

2
backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs

@ -246,7 +246,9 @@ public abstract class EventStoreTests<T> where T : IEventStore
CreateEventData(i * j)
};
#pragma warning disable MA0040 // Forward the CancellationToken parameter to methods that take one
await Sut.AppendAsync(Guid.NewGuid(), fullStreamName, EtagVersion.Any, commit1);
#pragma warning restore MA0040 // Forward the CancellationToken parameter to methods that take one
}
});
});

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

@ -7,12 +7,14 @@
using System.Collections.Concurrent;
#pragma warning disable MA0040 // Forward the CancellationToken parameter to methods that take one
namespace Squidex.Infrastructure.Tasks;
public class SchedulerTests
{
private readonly ConcurrentBag<int> actuals = new ConcurrentBag<int>();
private Scheduler sut = new Scheduler();
private readonly Scheduler sut = new Scheduler();
[Fact]
public async Task Should_schedule_single_task()

12
backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs

@ -99,7 +99,7 @@ public class SchemaResolverTests : GivenContext
await sut.OnActionExecutionAsync(actionExecutingContext, next);
AssertSchema(Schema);
AssertSchema();
}
[Fact]
@ -125,7 +125,7 @@ public class SchemaResolverTests : GivenContext
await sut.OnActionExecutionAsync(actionExecutingContext, next);
AssertSchema(Schema);
AssertSchema();
}
[Fact]
@ -140,7 +140,7 @@ public class SchemaResolverTests : GivenContext
await sut.OnActionExecutionAsync(actionExecutingContext, next);
AssertSchema(Schema);
AssertSchema();
}
[Fact]
@ -153,7 +153,7 @@ public class SchemaResolverTests : GivenContext
await sut.OnActionExecutionAsync(actionExecutingContext, next);
AssertSchema(Schema);
AssertSchema();
}
[Fact]
@ -168,7 +168,7 @@ public class SchemaResolverTests : GivenContext
await sut.OnActionExecutionAsync(actionExecutingContext, next);
AssertSchema(Schema);
AssertSchema();
}
[Fact]
@ -202,7 +202,7 @@ public class SchemaResolverTests : GivenContext
Assert.False(isNextCalled);
}
private void AssertSchema(ISchemaEntity schema)
private void AssertSchema()
{
Assert.Equal(Schema, actionContext.HttpContext.Features.Get<ISchemaFeature>()!.Schema);
Assert.True(isNextCalled);

192
frontend/src/app/features/content/pages/contents/contents-page.component.html

@ -40,108 +40,110 @@
</ng-container>
<ng-container>
<ng-container *ngIf="tableSettings.listFields | async; let listFields">
<sqx-list-view [isLoading]="contentsState.isLoading | async" [syncedHeader]="true" [tableNoPadding]="true">
<ng-container topHeader>
<div class="selection" *ngIf="selectionCount > 0">
{{ 'contents.selectionCount' | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp;
<button type="button" class="btn btn-outline-secondary btn-status me-2" *ngFor="let status of selectionStatuses | sqxKeys" (click)="changeSelectedStatus(status)">
<sqx-content-status layout="text"
[status]="status"
[statusColor]="selectionStatuses[status]">
</sqx-content-status>
</button>
<button type="button" class="btn btn-danger" *ngIf="selectionCanDelete"
(sqxConfirmClick)="deleteSelected()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteManyConfirmText"
confirmRememberKey="deleteContents">
{{ 'common.delete' | sqxTranslate }}
</button>
</div>
<ng-container *ngIf="tableSettings | async; let tableSettings">
<ng-container *ngIf="tableSettings.listFields | async; let tableFields">
<sqx-list-view [isLoading]="contentsState.isLoading | async" [syncedHeader]="true" [tableNoPadding]="true">
<ng-container topHeader>
<div class="selection" *ngIf="selectionCount > 0">
{{ 'contents.selectionCount' | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp;
<button type="button" class="btn btn-outline-secondary btn-status me-2" *ngFor="let status of selectionStatuses | sqxKeys" (click)="changeSelectedStatus(status)">
<sqx-content-status layout="text"
[status]="status"
[statusColor]="selectionStatuses[status]">
</sqx-content-status>
</button>
<button type="button" class="btn btn-danger" *ngIf="selectionCanDelete"
(sqxConfirmClick)="deleteSelected()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteManyConfirmText"
confirmRememberKey="deleteContents">
{{ 'common.delete' | sqxTranslate }}
</button>
</div>
<div class="settings-container">
<button type="button" class="btn btn-sm settings-button" (click)="tableViewModal.toggle()" #buttonSettings>
<i class="icon-settings"></i>
</button>
<div class="settings-container">
<button type="button" class="btn btn-sm settings-button" (click)="tableViewModal.toggle()" #buttonSettings>
<i class="icon-settings"></i>
</button>
<ng-container *sqxModal="tableViewModal">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonSettings" [scrollY]="true" position="bottom-right">
<sqx-custom-view-editor
[allFields]="tableSettings.schemaFields"
(listFieldsChange)="tableSettings.updateFields($event)"
[listFields]="$any(tableSettings.listFields| async)"
(reset)="tableSettings.reset()">
</sqx-custom-view-editor>
</sqx-dropdown-menu>
</ng-container>
</div>
</ng-container>
<ng-container *sqxModal="tableViewModal">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonSettings" [scrollY]="true" position="bottom-right">
<sqx-custom-view-editor
[allFields]="tableSettings.schemaFields"
(listFieldsChange)="tableSettings.updateFields($event)"
[listFields]="$any(tableFields)"
(reset)="tableSettings.reset()">
</sqx-custom-view-editor>
</sqx-dropdown-menu>
</ng-container>
</div>
</ng-container>
<ng-container header>
<table class="table table-items table-fixed" [sqxContentListWidth]="listFields" [fields]="tableSettings" #header>
<thead>
<tr>
<th class="cell-select">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="all_selected"
[ngModel]="selectedAll"
(ngModelChange)="selectAll($event)">
<ng-container header>
<table class="table table-items table-fixed" [sqxContentListWidth]="tableFields" [fields]="tableSettings" #header>
<thead>
<tr>
<th class="cell-select">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="all_selected"
[ngModel]="selectedAll"
(ngModelChange)="selectAll($event)">
<label class="form-check-label" for="all_selected"></label>
</div>
</th>
<th class="cell-actions cell-actions-left">
<span class="truncate">{{ 'common.actions' | sqxTranslate }}</span>
</th>
<th *ngFor="let field of listFields"
sqxContentListCell
sqxContentListCellResize
[field]="field"
[fields]="tableSettings">
<sqx-content-list-header
<label class="form-check-label" for="all_selected"></label>
</div>
</th>
<th class="cell-actions cell-actions-left">
<span class="truncate">{{ 'common.actions' | sqxTranslate }}</span>
</th>
<th *ngFor="let field of tableFields"
sqxContentListCell
sqxContentListCellResize
[field]="field"
(queryChange)="search($event)"
[query]="(contentsState.query | async)!"
[language]="language">
</sqx-content-list-header>
</th>
<th></th>
</tr>
</thead>
</table>
</ng-container>
<ng-container>
<div class="table-container">
<table class="table table-center table-fixed" [sqxContentListWidth]="listFields" [fields]="tableSettings" [sqxSyncWidth]="header">
<tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent"
[sqxContent]="content"
(clone)="clone(content)"
[cloneable]="contentsState.snapshot.canCreate"
(delete)="delete(content)"
[language]="language"
[languages]="languages"
[link]="[content.id, 'history']"
[schema]="schema"
[selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)"
(statusChange)="changeStatus(content, $event)"
[tableFields]="listFields"
[tableSettings]="tableSettings">
</tbody>
[fields]="tableSettings">
<sqx-content-list-header
[field]="field"
(queryChange)="search($event)"
[query]="(contentsState.query | async)!"
[language]="language">
</sqx-content-list-header>
</th>
<th></th>
</tr>
</thead>
</table>
</div>
</ng-container>
</ng-container>
<ng-container>
<div class="table-container">
<table class="table table-center table-fixed" [sqxContentListWidth]="tableFields" [fields]="tableSettings" [sqxSyncWidth]="header">
<tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent"
[sqxContent]="content"
(clone)="clone(content)"
[cloneable]="contentsState.snapshot.canCreate"
(delete)="delete(content)"
[language]="language"
[languages]="languages"
[link]="[content.id, 'history']"
[schema]="schema"
[selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)"
(statusChange)="changeStatus(content, $event)"
[tableFields]="tableFields"
[tableSettings]="tableSettings">
</tbody>
</table>
</div>
</ng-container>
<ng-container footer>
<sqx-pager (loadTotal)="reloadTotal()" [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager>
</ng-container>
</sqx-list-view>
</ng-container>
</ng-container>
<ng-container footer>
<sqx-pager (loadTotal)="reloadTotal()" [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager>
</ng-container>
</sqx-list-view>
</ng-container>
</ng-container>
</ng-container>
<ng-container sidebarMenu>
<div class="panel-nav">

72
frontend/src/app/features/content/pages/contents/contents-page.component.ts

@ -9,8 +9,9 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
import { AppLanguageDto, AppsState, ContentDto, ContentsState, contentsTranslationStatus, ContributorsState, defined, LanguagesState, LocalStoreService, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, Settings, switchSafe, TableSettings, TempService, TranslationStatus, UIState } from '@app/shared';
import { combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
import { AppLanguageDto, AppsState, ContentDto, ContentsService, ContentsState, contentsTranslationStatus, ContributorsState, defined, isDataField, LanguagesState, LocalStoreService, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, Settings, switchSafe, TableSettings, TempService, TranslationStatus, Types, UIState } from '@app/shared';
import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component';
@Component({
@ -27,7 +28,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public schema!: SchemaDto;
public tableSettings!: TableSettings;
public tableSettings!: Observable<TableSettings>;
public tableViewModal = new ModalModel();
public searchModal = new ModalModel();
@ -61,6 +62,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public readonly languagesState: LanguagesState,
private readonly appsState: AppsState,
private readonly contributorsState: ContributorsState,
private readonly contentsService: ContentsService,
private readonly localStore: LocalStoreService,
private readonly route: ActivatedRoute,
private readonly router: Router,
@ -89,29 +91,46 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.languages = languages;
}));
this.own(
getSchemaName(this.route).pipe(switchMap(() => this.schemasState.selectedSchema.pipe(defined(), take(1))))
.subscribe(schema => {
this.resetSelection();
const schema$ =
getSchemaName(this.route).pipe(switchMap(() => this.schemasState.selectedSchema.pipe(defined(), take(1), shareReplay(1))));
this.schema = schema;
const tableSetting$ =
schema$.pipe(map(s => new TableSettings(this.uiState, s)), shareReplay(1));
const initial =
this.contentsRoute.mapTo(this.contentsState)
.withPaging('contents', 10)
.withSynchronizer(QuerySynchronizer.INSTANCE)
.getInitial();
const tableField$ =
tableSetting$.pipe(switchMap(s => s.listFields));
this.contentsState.load(false, true, initial);
this.contentsRoute.listen();
const tableName$ =
tableField$.pipe(map(t => t.map(x => x.name).filter(isDataField).sorted()), distinctUntilChanged(Types.equals));
this.tableSettings = new TableSettings(this.uiState, schema);
this.tableSettings = tableSetting$;
const languageKey = this.localStore.get(this.languageKey());
const language = this.languages.find(x => x.iso2Code === languageKey);
if (language) {
this.language = language;
this.own(
combineLatest([schema$, tableName$])
.subscribe(([schema, fieldNames]) => {
if (!this.schema) {
this.resetSelection();
this.schema = schema;
const initial =
this.contentsRoute.mapTo(this.contentsState)
.withPaging('contents', 10)
.withSynchronizer(QuerySynchronizer.INSTANCE)
.getInitial();
this.contentsState.load(false, true, { fieldNames, ...initial });
this.contentsRoute.listen();
const languageKey = this.localStore.get(this.languageKey());
const languageItem = this.languages.find(x => x.iso2Code === languageKey);
if (languageItem) {
this.language = languageItem;
}
} else {
this.contentsState.setFieldNames(fieldNames);
this.contentsState.load();
}
}));
@ -162,9 +181,16 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
}
public clone(content: ContentDto) {
this.tempService.put(content.data);
if (!content) {
return;
}
this.contentsService.getContent(this.contentsState.appName, this.contentsState.schemaName, content.id)
.subscribe(currentContent => {
this.tempService.put(currentContent.data);
this.router.navigate(['new'], { relativeTo: this.route });
this.router.navigate(['new'], { relativeTo: this.route });
});
}
public changeLanguage(language: AppLanguageDto) {

4
frontend/src/app/features/content/pages/contents/custom-view-editor.component.html

@ -17,7 +17,7 @@
<div class="form-check">
<input class="form-check-input" type="checkbox" checked (click)="removeField(field)" id="field_{{field}}" [disabled]="!field">
<label class="form-check-label" for="field_{{field}}">
{{field.label | sqxTranslate}} ({{field.name}})
{{(field.title || field.label) | sqxTranslate}}: <code>{{field.name}}</code>
</label>
</div>
</div>
@ -33,7 +33,7 @@
<div class="form-check">
<input class="form-check-input" type="checkbox" (click)="addField(field)" id="field_{{field}}">
<label class="form-check-label" for="field_{{field}}">
{{field.label | sqxTranslate}} ({{field.name}})
{{(field.title || field.label) | sqxTranslate}}: <code>{{field.name}}</code>
</label>
</div>
</div>

8
frontend/src/app/features/schemas/pages/schema/ui/field-list.component.html

@ -9,8 +9,8 @@
<sqx-form-alert>{{emptyText}}</sqx-form-alert>
</div>
<div *ngFor="let field of fieldsAdded" class="table-items-row table-items-row-summary" cdkDrag>
<i class="icon-drag2 drag-handle"></i> <span>{{field.name}}</span>
<div *ngFor="let field of fieldsAdded" class="table-items-row table-items-row-summary truncate" cdkDrag>
<i class="icon-drag2 drag-handle"></i> {{(field.title || field.label) | sqxTranslate}}: <code>{{field.name}}</code>
</div>
</div>
@ -22,7 +22,7 @@
(cdkDropListDropped)="drop($event)">
<label>{{ 'schemas.ui.unassignedFields' | sqxTranslate }}</label>
<div *ngFor="let field of fieldsNotAdded" class="table-items-row table-items-row-summary" cdkDrag>
<i class="icon-drag2 drag-handle"></i> <span>{{field.name}}</span>
<div *ngFor="let field of fieldsNotAdded" class="table-items-row table-items-row-summary truncate" cdkDrag>
<i class="icon-drag2 drag-handle"></i> {{(field.title || field.label) | sqxTranslate}}: <code>{{field.name}}</code>
</div>
</div>

9
frontend/src/app/shared/services/contents.service.ts

@ -12,7 +12,7 @@ import { map } from 'rxjs/operators';
import { ApiUrlConfig, DateTime, ErrorDto, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework';
import { StatusInfo } from './../state/contents.state';
import { Query, sanitize } from './query';
import { parseField, RootFieldDto } from './schemas.service';
import { isDataField, parseField, RootFieldDto } from './schemas.service';
export class ScheduleDto {
constructor(
@ -163,6 +163,9 @@ export type ContentsQuery = Readonly<{
// True, to not return the total number of items, if the query would be slow.
noSlowTotal?: boolean;
// The field names to query.
fieldNames?: ReadonlyArray<string>;
}>;
export type ContentsByIds = Readonly<{
@ -370,6 +373,10 @@ function buildHeaders(q: ContentsQuery | undefined) {
headers: {},
};
if (q?.fieldNames) {
options.headers['X-Fields'] = q.fieldNames.filter(isDataField).join(',');
}
if (q?.noTotal) {
options.headers['X-NoTotal'] = '1';
}

25
frontend/src/app/shared/services/schemas.service.ts

@ -22,61 +22,79 @@ export const META_FIELDS = {
empty: {
name: '',
label: '',
title: '',
},
id: {
name: 'meta.id',
label: 'i18n:schemas.tableHeaders.id',
title: 'i18n:schemas.tableHeaders.id_title',
},
created: {
name: 'meta.created',
label: 'i18n:schemas.tableHeaders.created',
title: 'i18n:schemas.tableHeaders.created_title',
},
createdByAvatar: {
name: 'meta.createdBy.avatar',
label: 'i18n:schemas.tableHeaders.createdByShort',
title: 'i18n:schemas.tableHeaders.createdByShort_title',
},
createdByName: {
name: 'meta.createdBy.name',
label: 'i18n:schemas.tableHeaders.createdBy',
title: 'i18n:schemas.tableHeaders.createdBy_title',
},
lastModified: {
name: 'meta.lastModified',
label: 'i18n:schemas.tableHeaders.lastModified',
title: 'i18n:schemas.tableHeaders.lastModified_title',
},
lastModifiedByAvatar: {
name: 'meta.lastModifiedBy.avatar',
label: 'i18n:schemas.tableHeaders.lastModifiedByShort',
title: 'i18n:schemas.tableHeaders.lastModifiedByShort_title',
},
lastModifiedByName: {
name: 'meta.lastModifiedBy.name',
label: 'i18n:schemas.tableHeaders.lastModifiedBy',
title: 'i18n:schemas.tableHeaders.lastModifiedBy_title',
},
status: {
name: 'meta.status',
label: 'i18n:schemas.tableHeaders.status',
title: 'i18n:schemas.tableHeaders.status_title',
},
statusColor: {
name: 'meta.status.color',
label: 'i18n:schemas.tableHeaders.status',
title: 'i18n:schemas.tableHeaders.status_title',
},
statusNext: {
name: 'meta.status.next',
label: 'i18n:schemas.tableHeaders.nextStatus',
title: 'i18n:schemas.tableHeaders.nextStatus_title',
},
version: {
name: 'meta.version',
label: 'i18n:schemas.tableHeaders.version',
title: 'i18n:schemas.tableHeaders.version_title',
},
translationStatus: {
name: 'meta.translationStatus',
label: 'i18n:schemas.tableHeaders.translationStatus',
title: 'i18n:schemas.tableHeaders.translationStatus_title',
},
translationStatusAverage: {
name: 'meta.translationStatusAverage',
label: 'i18n:schemas.tableHeaders.translationStatusAverage',
title: 'i18n:schemas.tableHeaders.translationStatusAverage_title',
},
};
export function isDataField(name: string) {
return name.indexOf('meta.') < 0;
}
export const FIELD_RULE_ACTIONS: ReadonlyArray<FieldRuleAction> = [
'Disable',
'Hide',
@ -151,7 +169,9 @@ export class SchemaDto {
this.displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
function tableField(rootField: RootFieldDto) {
return { name: rootField.name, label: rootField.displayName, rootField };
const label = rootField.displayName;
return { name: rootField.name, label, rootField };
}
if (fields) {
@ -363,6 +383,9 @@ export type TableField = Readonly<{
// The label for the table header.
label: string;
// The title.
title?: string;
// The reference to the root field.
rootField?: RootFieldDto;
}>;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save