Browse Source

Generate schema with ai (#1238)

* Fix prepare step.

* UI fixes.

* Small fixes.

* Generate with AI.

* Update packages.
pull/1241/head
Sebastian Stehle 10 months ago
committed by GitHub
parent
commit
79e16cb12b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs
  2. 10
      backend/i18n/frontend_en.json
  3. 6
      backend/i18n/frontend_fr.json
  4. 6
      backend/i18n/frontend_it.json
  5. 6
      backend/i18n/frontend_nl.json
  6. 6
      backend/i18n/frontend_pt.json
  7. 6
      backend/i18n/frontend_zh.json
  8. 10
      backend/i18n/source/frontend_en.json
  9. 14
      backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj
  10. 12
      backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj
  11. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  12. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  13. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  14. 79
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AIQueryCache.cs
  15. 98
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/SchemaAIGenerator.cs
  16. 14
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/SchemaAIResult.cs
  17. 41
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/SessionFactory.cs
  18. 21
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/StringLogger.cs
  19. 67
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs
  20. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Entities/ContextHeaders.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasOptions.cs
  23. 3
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  24. 21
      backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs
  25. 2
      backend/src/Squidex.Infrastructure/Queries/FilterSchema.cs
  26. 14
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  27. 2
      backend/src/Squidex.Web/Services/StringLocalizer.cs
  28. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs
  29. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs
  30. 29
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/GenerateSchemaDto.cs
  31. 30
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/GenerateSchemaResponseDto.cs
  32. 30
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  33. 3
      backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs
  34. 10
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  35. 62
      backend/src/Squidex/Config/Startup/LogConfigurationHost.cs
  36. 84
      backend/src/Squidex/Configuration/schemas__generateprompt
  37. 2
      backend/src/Squidex/Program.cs
  38. 33
      backend/src/Squidex/Squidex.csproj
  39. 10
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs
  40. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs
  41. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs
  42. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs
  43. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs
  44. 16
      frontend/.storybook/main.js
  45. 2
      frontend/src/app/features/apps/pages/apps-page.component.html
  46. 2
      frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html
  47. 217
      frontend/src/app/features/schemas/pages/schemas/schema-form.component.html
  48. 85
      frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts
  49. 2
      frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html
  50. 8
      frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts
  51. 8
      frontend/src/app/framework/angular/forms/control-errors.component.ts
  52. 5
      frontend/src/app/framework/angular/forms/editors/code-editor.component.scss
  53. 14
      frontend/src/app/framework/angular/forms/editors/code-editor.component.ts
  54. 18
      frontend/src/app/framework/angular/forms/editors/code-editor.stories.ts
  55. 3
      frontend/src/app/framework/angular/forms/model.ts
  56. 2
      frontend/src/app/framework/angular/modals/dialog-renderer.component.html
  57. 1
      frontend/src/app/framework/angular/stateful.component.ts
  58. 4
      frontend/src/app/shared/components/app-form.component.html
  59. 2
      frontend/src/app/shared/components/app-form.component.scss
  60. 2
      frontend/src/app/shared/components/team-form.component.html
  61. 153
      frontend/src/app/shared/model/generated.ts
  62. 21
      frontend/src/app/shared/services/schemas.service.spec.ts
  63. 12
      frontend/src/app/shared/services/schemas.service.ts
  64. 12
      frontend/src/app/shared/state/schemas.forms.ts
  65. 8
      frontend/src/app/shared/state/schemas.state.ts
  66. 6
      frontend/src/app/theme/_bootstrap.scss
  67. 7
      frontend/src/app/theme/_forms.scss
  68. 5
      tools/.editorconfig
  69. 6
      tools/TestSuite/TestSuite.ApiTests/AnonymousTests.cs
  70. 4
      tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs
  71. 6
      tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs
  72. 2
      tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs
  73. 16
      tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs
  74. 10
      tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs
  75. 10
      tools/TestSuite/TestSuite.ApiTests/AppTests.cs
  76. 8
      tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs
  77. 8
      tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs
  78. 10
      tools/TestSuite/TestSuite.ApiTests/AssetScriptingTests.cs
  79. 54
      tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  80. 10
      tools/TestSuite/TestSuite.ApiTests/BackupTests.cs
  81. 10
      tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs
  82. 10
      tools/TestSuite/TestSuite.ApiTests/ContentCollationTests.cs
  83. 18
      tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs
  84. 6
      tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs
  85. 138
      tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs
  86. 42
      tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs
  87. 28
      tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs
  88. 210
      tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs
  89. 60
      tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs
  90. 6
      tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs
  91. 60
      tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs
  92. 8
      tools/TestSuite/TestSuite.ApiTests/RuleEventsTests.cs
  93. 148
      tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs
  94. 44
      tools/TestSuite/TestSuite.ApiTests/RuleTests.cs
  95. 2
      tools/TestSuite/TestSuite.ApiTests/RuleValidationTests.cs
  96. 22
      tools/TestSuite/TestSuite.ApiTests/SchemaTests.cs
  97. 4
      tools/TestSuite/TestSuite.ApiTests/SearchTests.cs
  98. 4
      tools/TestSuite/TestSuite.ApiTests/TeamContributorTests.cs
  99. 14
      tools/TestSuite/TestSuite.ApiTests/TeamTests.cs
  100. 4
      tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

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

@ -48,12 +48,12 @@ internal sealed class CompositeUniqueValidator(string contentTag, IContentReposi
var found = await contentRepository.QueryIdsAsync(context.Root.App, context.Root.Schema, filter, SearchScope.All);
if (found.Any(x => x.Id != context.Root.ContentId))
{
context.AddError("A content with the same values already exist.", Enumerable.Empty<string>());
context.AddError("A content with the same values already exist.", []);
}
}
}
private static ClrValue? TryGetValue(IRootField field, ContentData data)
private static ClrValue? TryGetValue(RootField field, ContentData data)
{
var value = JsonValue.Null;

10
backend/i18n/frontend_en.json

@ -14,7 +14,7 @@
"apps.appLoadFailed": "Failed to load app. Please reload.",
"apps.appNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.",
"apps.appNameValidationMessage": "Name can contain lower case letters (a-z), numbers and dashes between.",
"apps.appNameWarning": "The app name cannot be changed later.",
"apps.appNameWarning": "The name is used to identify your app in every HTTP request. It must be unique and cannot be changed once set.",
"apps.appsButtonCreate": "Create App",
"apps.appsButtonCreateTeam": "Create Team",
"apps.appsButtonFallbackTitle": "Apps and Teams",
@ -339,6 +339,7 @@
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
"common.prompt": "Prompt",
"common.properties": "Properties",
"common.queryOperators.contains": "contains",
"common.queryOperators.empty": "is empty",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.create": "Create Schema",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Create new category...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Failed to create schema. Please reload.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "New Schema",
"schemas.deleteConfirmText": "Do you really want to delete the schema?",
"schemas.deleteConfirmTitle": "Delete schema",
@ -1007,12 +1011,14 @@
"schemas.modeMultipleDescription": "Best for multiple instances like blog posts, pages, authors, products...",
"schemas.modeSingle": "Single content",
"schemas.modeSingleDescription": "Best for single instances like the home page, privacy policies, settings...",
"schemas.nameWarning": "These values cannot be changed later.",
"schemas.nameWarning": "The name is used to identify your schema in every HTTP request. It must be unique within an app and cannot be changed once set.",
"schemas.previewUrls.empty": "No preview urls configured.",
"schemas.previewUrls.help": "Checkout the integrated help page to learn more about preview URL's.",
"schemas.previewUrls.namePlaceholder": "Web or Mobile",
"schemas.previewUrls.title": "Preview URLs",
"schemas.previewUrls.urlPlaceholder": "URL with variables",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Published",
"schemas.publishFailed": "Failed to publish schema. Please reload.",
"schemas.referenceFields": "Reference Fields",

6
backend/i18n/frontend_fr.json

@ -339,6 +339,7 @@
"common.prevPage": "Previous Page",
"common.product": "CMS sans tête Squidex",
"common.project": "Projet",
"common.prompt": "Prompt",
"common.properties": "Properties",
"common.queryOperators.contains": "contient",
"common.queryOperators.empty": "est vide",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Contenu de l'extension de la barre latérale",
"schemas.contentsSidebarUrlHint": "URL du plug-in pour la barre latérale dans la vue de liste.",
"schemas.create": "Créer un schéma",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Créer une nouvelle catégorie...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Échec de la création du schéma. Veuillez recharger.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "Nouveau schéma",
"schemas.deleteConfirmText": "Voulez-vous vraiment supprimer le schéma\u00A0?",
"schemas.deleteConfirmTitle": "Supprimer le schéma",
@ -1013,6 +1017,8 @@
"schemas.previewUrls.namePlaceholder": "Web ou mobile",
"schemas.previewUrls.title": "Aperçu des URL",
"schemas.previewUrls.urlPlaceholder": "URL avec variables",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Publié",
"schemas.publishFailed": "Échec de la publication du schéma. Veuillez recharger.",
"schemas.referenceFields": "Champs de référence",

6
backend/i18n/frontend_it.json

@ -339,6 +339,7 @@
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Progetto",
"common.prompt": "Prompt",
"common.properties": "Properties",
"common.queryOperators.contains": "contiene",
"common.queryOperators.empty": "è vuoto",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Estensione della barra di navigazione laterale (liste)",
"schemas.contentsSidebarUrlHint": "URL del plug-in per la barra di navigazione laterale nella visualizzazione delle liste.",
"schemas.create": "Crea uno Schema",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Crea una nuova categoria...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Non è stato possibile creare lo schema. Per favore ricarica.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "Nuovo Schema",
"schemas.deleteConfirmText": "Sei sicuro di voler eliminare lo schema?",
"schemas.deleteConfirmTitle": "Cancella lo schema",
@ -1013,6 +1017,8 @@
"schemas.previewUrls.namePlaceholder": "Web o Mobile",
"schemas.previewUrls.title": "URL dell'anteprima",
"schemas.previewUrls.urlPlaceholder": "URL con variabili",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Pubblicato",
"schemas.publishFailed": "Non è stato possibile pubblicare lo schema. Per favore ricarica.",
"schemas.referenceFields": "Campi per i collegamenti (riferimenti)",

6
backend/i18n/frontend_nl.json

@ -339,6 +339,7 @@
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
"common.prompt": "Prompt",
"common.properties": "Properties",
"common.queryOperators.contains": "bevat",
"common.queryOperators.empty": "is leeg",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Inhoud zijbalk uitbreiding",
"schemas.contentsSidebarUrlHint": "URL naar de plug-in voor de zijbalk in de lijstweergave.",
"schemas.create": "Schema maken",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Nieuwe categorie maken ...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Kan schema niet maken. Laad opnieuw.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "Nieuw schema",
"schemas.deleteConfirmText": "Weet je zeker dat je het schema wilt verwijderen?",
"schemas.deleteConfirmTitle": "Schema verwijderen",
@ -1013,6 +1017,8 @@
"schemas.previewUrls.namePlaceholder": "Web of mobiel",
"schemas.previewUrls.title": "Voorbeeld-URL's",
"schemas.previewUrls.urlPlaceholder": "URL met variabelen",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Gepubliceerd",
"schemas.publishFailed": "Kan schema niet publiceren. Laad opnieuw.",
"schemas.referenceFields": "Referentievelden",

6
backend/i18n/frontend_pt.json

@ -339,6 +339,7 @@
"common.prevPage": "Previous Page",
"common.product": "CMS Headless Squidex",
"common.project": "Projeto",
"common.prompt": "Prompt",
"common.properties": "Properties",
"common.queryOperators.contains": "contém",
"common.queryOperators.empty": "está vazio",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Extensão da barra lateral de conteúdo",
"schemas.contentsSidebarUrlHint": "URL para o plugin para a barra lateral na vista da lista.",
"schemas.create": "Criar Esquema",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Criar nova categoria...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Falhou em criar esquema. Por favor, recarregue.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "Novo esquema",
"schemas.deleteConfirmText": "Quer mesmo apagar o esquema?",
"schemas.deleteConfirmTitle": "Remover Esquema",
@ -1013,6 +1017,8 @@
"schemas.previewUrls.namePlaceholder": "Web ou Mobile",
"schemas.previewUrls.title": "URLs de pré-visualização",
"schemas.previewUrls.urlPlaceholder": "URL com variáveis",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Publicado",
"schemas.publishFailed": "Falhou em publicar o esquema. Por favor, recarregue.",
"schemas.referenceFields": "Campos de Referência",

6
backend/i18n/frontend_zh.json

@ -339,6 +339,7 @@
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "项目",
"common.prompt": "Prompt",
"common.properties": "Properties",
"common.queryOperators.contains": "包含",
"common.queryOperators.empty": "为空",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "内容侧边栏扩展",
"schemas.contentsSidebarUrlHint": "列表视图中侧边栏插件的 URL。",
"schemas.create": "Create Schema",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "创建新类别...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "无法创建Schemas。请重新加载。",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "新Schemas",
"schemas.deleteConfirmText": "您真的要删除Schemas吗?",
"schemas.deleteConfirmTitle": "删除Schemas",
@ -1013,6 +1017,8 @@
"schemas.previewUrls.namePlaceholder": "网络或移动",
"schemas.previewUrls.title": "预览网址",
"schemas.previewUrls.urlPlaceholder": "带变量的 URL",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Published",
"schemas.publishFailed": "无法发布Schemas。请重新加载。",
"schemas.referenceFields": "参考字段",

10
backend/i18n/source/frontend_en.json

@ -14,7 +14,7 @@
"apps.appLoadFailed": "Failed to load app. Please reload.",
"apps.appNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.",
"apps.appNameValidationMessage": "Name can contain lower case letters (a-z), numbers and dashes between.",
"apps.appNameWarning": "The app name cannot be changed later.",
"apps.appNameWarning": "The name is used to identify your app in every HTTP request. It must be unique and cannot be changed once set.",
"apps.appsButtonCreate": "Create App",
"apps.appsButtonCreateTeam": "Create Team",
"apps.appsButtonFallbackTitle": "Apps and Teams",
@ -339,6 +339,7 @@
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
"common.prompt": "Prompt",
"common.properties": "Properties",
"common.queryOperators.contains": "contains",
"common.queryOperators.empty": "is empty",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.create": "Create Schema",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Create new category...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Failed to create schema. Please reload.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "New Schema",
"schemas.deleteConfirmText": "Do you really want to delete the schema?",
"schemas.deleteConfirmTitle": "Delete schema",
@ -1007,12 +1011,14 @@
"schemas.modeMultipleDescription": "Best for multiple instances like blog posts, pages, authors, products...",
"schemas.modeSingle": "Single content",
"schemas.modeSingleDescription": "Best for single instances like the home page, privacy policies, settings...",
"schemas.nameWarning": "These values cannot be changed later.",
"schemas.nameWarning": "The name is used to identify your schema in every HTTP request. It must be unique within an app and cannot be changed once set.",
"schemas.previewUrls.empty": "No preview urls configured.",
"schemas.previewUrls.help": "Checkout the integrated help page to learn more about preview URL's.",
"schemas.previewUrls.namePlaceholder": "Web or Mobile",
"schemas.previewUrls.title": "Preview URLs",
"schemas.previewUrls.urlPlaceholder": "URL with variables",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Published",
"schemas.publishFailed": "Failed to publish schema. Please reload.",
"schemas.referenceFields": "Reference Fields",

14
backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj

@ -40,13 +40,13 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="8.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.NetTopologySuite" Version="8.0.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.25.0" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="7.25.0" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="7.25.0" />
<PackageReference Include="Squidex.Hosting" Version="7.25.0" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.25.0" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.26.0" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

12
backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj

@ -25,12 +25,12 @@
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.30.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.25.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.25.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.25.0" />
<PackageReference Include="Squidex.Hosting" Version="7.25.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.25.0" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

2
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -20,7 +20,7 @@
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Flows" Version="7.25.0" />
<PackageReference Include="Squidex.Flows" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

4
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -29,8 +29,8 @@
<PackageReference Include="NJsonSchema" Version="11.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="7.25.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.25.0" />
<PackageReference Include="Squidex.AI" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

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

@ -105,7 +105,7 @@ public sealed class ContentValidator
return new ObjectValidator<ContentFieldData>(fieldValidators, isPartial, "field");
}
private IValidator CreateFieldValidator(IRootField field, bool isPartial)
private IValidator CreateFieldValidator(RootField field, bool isPartial)
{
var valueValidator = CreateValueValidator(field);

79
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AIQueryCache.cs

@ -0,0 +1,79 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text;
using IdentityModel;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using Squidex.CLI.Commands.Implementation.AI;
using Squidex.Infrastructure.ObjectPool;
namespace Squidex.Domain.Apps.Entities.Apps.Templates;
public sealed class AIQueryCache(IDistributedCache distributedCache) : IQueryCache
{
private readonly DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30),
};
public async Task<GeneratedContent?> GetAsync(string prompt,
CancellationToken ct = default)
{
var cached = await distributedCache.GetAsync(CacheKey(prompt), ct);
if (cached == null)
{
return default!;
}
try
{
// Use newtonsoft JSON because the CLI still deals with this library and uses JTokens.
using var cacheStream = new MemoryStream(cached);
using var cacheReader = new StreamReader(cacheStream);
using var jsonReader = new JsonTextReader(cacheReader);
var serializer = new JsonSerializer();
return serializer.Deserialize<GeneratedContent>(jsonReader);
}
catch
{
return default!;
}
}
public async Task StoreAsync(string prompt, GeneratedContent content,
CancellationToken ct)
{
try
{
// Use newtonsoft JSON because the CLI still deals with this library and uses JTokens.
using var cacheStream = DefaultPools.MemoryStream.GetStream();
#pragma warning disable MA0042 // Do not use blocking calls in an async method
using var cacheWriter = new StreamWriter(cacheStream, Encoding.UTF8, leaveOpen: true);
using (var jsonWriter = new JsonTextWriter(cacheWriter))
{
var serializer = new JsonSerializer();
serializer.Serialize(jsonWriter, content);
jsonWriter.Flush();
}
#pragma warning restore MA0042 // Do not use blocking calls in an async method
await distributedCache.SetAsync(CacheKey(prompt), cacheStream.ToArray(), cacheOptions, ct);
}
catch
{
return;
}
}
private static string CacheKey(string prompt)
{
return $"AI_{prompt.ToSha512()}";
}
}

98
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/SchemaAIGenerator.cs

@ -0,0 +1,98 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using ConsoleTables;
using Microsoft.Extensions.Options;
using Squidex.AI.Implementation.OpenAI;
using Squidex.CLI.Commands.Implementation.AI;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Entities.Apps.Templates;
public sealed class SchemaAIGenerator(
IQueryCache queryCache,
SessionFactory sessionFactory,
IOptions<SchemasOptions> schemasOptions,
IOptions<OpenAIChatOptions> openAIOptions)
{
private const int MaxContentItems = 20;
public async Task<SchemaAIResult> ExecuteAsync(App app, string prompt, int numberOfContentItems, bool execute,
CancellationToken ct)
{
var apiKey = openAIOptions.Value.ApiKey;
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new NotSupportedException("OpenAI ApiKey not configured.");
}
var request = new GenerateRequest
{
Description = prompt,
GenerateImages = false,
NumberOfAttempts = 3,
NumberOfContentItems = Math.Min(MaxContentItems, numberOfContentItems),
OpenAIApiKey = apiKey,
SystemPrompt = schemasOptions.Value.GeneratePrompt,
};
var generator = new AIContentGenerator(queryCache);
var generated = await generator.GenerateAsync(request, ct);
using var cliLog = new StringLogger();
WriteSchema(generated, cliLog);
WriteContent(generated, cliLog);
if (execute)
{
var session = sessionFactory.CreateSession(app);
var executor = new AIContentExecutor(session, cliLog);
await executor.ExecuteAsync(request, generated, ct);
}
return new SchemaAIResult(cliLog.Lines.ToReadonlyList(), execute ? generated.Schema.Name : null);
}
private static void WriteSchema(GeneratedContent generated, StringLogger log)
{
log.WriteLine($"Schema Name: {generated.Schema.Name}");
log.WriteLine();
log.WriteLine("Schema Fields:");
var schemaTable = new ConsoleTable("Name", "Type", "Required", "Localized");
schemaTable.Options.EnableCount = false;
foreach (var field in generated.Schema.Fields)
{
schemaTable.AddRow(field.Name, field.Type, field.IsRequired, field.IsLocalized);
}
log.WriteLine(schemaTable.ToString());
}
private static void WriteContent(GeneratedContent generated, StringLogger log)
{
if (generated.Contents.Count > 0)
{
const int MaximumPreview = 3;
log.WriteLine();
log.WriteLine("Contents:");
log.WriteJson(generated.Contents.Take(MaximumPreview));
var more = generated.Contents.Count - MaximumPreview;
if (more > 0)
{
log.WriteLine($"+ {more} content items");
}
}
}
}

14
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/SchemaAIResult.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Collections;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Entities.Apps.Templates;
public sealed record SchemaAIResult(ReadonlyList<string> Log, string? SchemaName);

41
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/SessionFactory.cs

@ -0,0 +1,41 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Options;
using Squidex.CLI.Configuration;
using Squidex.ClientLibrary;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps.Templates;
public sealed class SessionFactory(IOptions<TemplatesOptions> templateOptions, IUrlGenerator urlGenerator)
{
private readonly TemplatesOptions options = templateOptions.Value;
public Session CreateSession(App app)
{
var client = app.Clients.First();
var url = options.LocalUrl;
if (string.IsNullOrEmpty(url))
{
url = urlGenerator.Root();
}
return new Session(
new DirectoryInfo(Path.GetTempPath()),
new SquidexClient(new SquidexOptions
{
IgnoreSelfSignedCertificates = true,
AppName = app.Name,
ClientId = $"{app.Name}:{client.Key}",
ClientSecret = client.Value.Secret,
Url = url,
}));
}
}

21
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/StringLogger.cs

@ -6,14 +6,15 @@
// ==========================================================================
using System.Globalization;
using Newtonsoft.Json;
using Squidex.CLI.Commands.Implementation;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.Apps.Templates;
public sealed class StringLogger(IJsonSerializer jsonSerializer) : ILogger, ILogLine
// Use newtonsoft JSON because the CLI still deals with this library and uses JTokens.
public sealed class StringLogger : ILogger, ILogLine
{
private const int MaxActionLength = 40;
private readonly List<string> lines = [];
@ -22,16 +23,18 @@ public sealed class StringLogger(IJsonSerializer jsonSerializer) : ILogger, ILog
public bool CanWriteToSameLine => false;
public List<string> Lines => lines;
public void Flush(ISemanticLog log, string template)
{
var mesage = string.Join('\n', lines);
var mesage = string.Join('\n', Lines);
log.LogInformation(w => w
.WriteProperty("message", $"CLI executed or template {template}.")
.WriteProperty("template", template)
.WriteArray("steps", a =>
{
foreach (var line in lines)
foreach (var line in Lines)
{
a.WriteValue(line);
}
@ -84,22 +87,22 @@ public sealed class StringLogger(IJsonSerializer jsonSerializer) : ILogger, ILog
public void WriteLine()
{
lines.Add(string.Empty);
Lines.Add(string.Empty);
}
public void WriteLine(string message)
{
lines.Add(message);
Lines.Add(message);
}
public void WriteLine(string message, params object?[] args)
{
lines.Add(string.Format(CultureInfo.InvariantCulture, message, args));
Lines.Add(string.Format(CultureInfo.InvariantCulture, message, args));
}
public void WriteJson(object message)
{
lines.Add(jsonSerializer.Serialize(message, true));
Lines.Add(JsonConvert.SerializeObject(message, Formatting.Indented));
}
private void AddToErrors(string reason)
@ -110,7 +113,7 @@ public sealed class StringLogger(IJsonSerializer jsonSerializer) : ILogger, ILog
private void AddToLine(string message)
{
startedLine += message;
lines.Add(startedLine);
Lines.Add(startedLine);
startedLine = string.Empty;
}

67
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Options;
using Squidex.CLI.Commands.Implementation;
using Squidex.CLI.Commands.Implementation.FileSystem;
using Squidex.CLI.Commands.Implementation.Sync;
@ -16,9 +15,6 @@ using Squidex.CLI.Commands.Implementation.Sync.Contents;
using Squidex.CLI.Commands.Implementation.Sync.Rules;
using Squidex.CLI.Commands.Implementation.Sync.Schemas;
using Squidex.CLI.Commands.Implementation.Sync.Workflows;
using Squidex.CLI.Configuration;
using Squidex.ClientLibrary;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
@ -28,15 +24,11 @@ using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.Apps.Templates;
public sealed class TemplateCommandMiddleware(
TemplatesClient templatesClient,
IOptions<TemplatesOptions> templateOptions,
IUrlGenerator urlGenerator,
IJsonSerializer jsonSerializer,
TemplatesClient client,
SessionFactory sessionFactory,
ISemanticLog log)
: ICommandMiddleware
{
private readonly TemplatesOptions templateOptions = templateOptions.Value;
public async Task HandleAsync(CommandContext context, NextDelegate next,
CancellationToken ct)
{
@ -55,7 +47,7 @@ public sealed class TemplateCommandMiddleware(
return;
}
var repository = await templatesClient.GetRepositoryUrl(template);
var repository = await client.GetRepositoryUrl(template);
if (string.IsNullOrEmpty(repository))
{
@ -65,17 +57,16 @@ public sealed class TemplateCommandMiddleware(
return;
}
using (var cliLog = new StringLogger(jsonSerializer))
var cliLog = new StringLogger();
try
{
try
{
var session = CreateSession(app);
var session = sessionFactory.CreateSession(app);
var syncService = await CreateSyncServiceAsync(repository, session);
var syncOptions = new SyncOptions();
var syncService = await CreateSyncServiceAsync(repository, session);
var syncOptions = new SyncOptions();
var targets = new ISynchronizer[]
{
var targets = new ISynchronizer[]
{
new AppSynchronizer(cliLog),
new AssetFoldersSynchronizer(cliLog),
new AssetsSynchronizer(cliLog),
@ -83,18 +74,17 @@ public sealed class TemplateCommandMiddleware(
new SchemasSynchronizer(cliLog),
new WorkflowsSynchronizer(cliLog),
new ContentsSynchronizer(cliLog),
};
};
foreach (var target in targets)
{
await target.ImportAsync(syncService, syncOptions, session);
}
}
finally
foreach (var target in targets)
{
cliLog.Flush(log, template);
await target.ImportAsync(syncService, syncOptions, session);
}
}
finally
{
cliLog.Flush(log, template);
}
}
private static async Task<ISyncService> CreateSyncServiceAsync(string repository, ISession session)
@ -103,27 +93,4 @@ public sealed class TemplateCommandMiddleware(
return new SyncService(fs, session);
}
private ISession CreateSession(App app)
{
var client = app.Clients.First();
var url = templateOptions.LocalUrl;
if (string.IsNullOrEmpty(url))
{
url = urlGenerator.Root();
}
return new Session(
new DirectoryInfo(Path.GetTempPath()),
new SquidexClient(new SquidexOptions
{
IgnoreSelfSignedCertificates = true,
AppName = app.Name,
ClientId = $"{app.Name}:{client.Key}",
ClientSecret = client.Value.Secret,
Url = url,
}));
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs

@ -5,14 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.CodeDom;
using System.Text.RegularExpressions;
using Markdig;
using Markdig.Renderers.Normalize;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using Microsoft.Extensions.Options;
using Squidex.ClientLibrary;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Templates;

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

@ -133,6 +133,6 @@ public static class ContextHeaders
return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Distinct();
}
return Enumerable.Empty<string>();
return [];
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasOptions.cs

@ -10,4 +10,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas;
public sealed class SchemasOptions
{
public bool DeletePermanent { get; set; }
public string? GeneratePrompt { get; set; }
}

3
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -27,6 +27,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ConsoleTables" Version="2.7.0" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="FFMpegCore" Version="5.1.0" />
<PackageReference Include="GraphQL" Version="8.2.1" />
@ -40,7 +41,7 @@
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Notifo.SDK" Version="1.7.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.CLI.Core" Version="13.9.0" />
<PackageReference Include="Squidex.CLI.Core" Version="13.13.0" />
<PackageReference Include="YDotNet" Version="0.4.3" />
<PackageReference Include="YDotNet.Extensions" Version="0.4.3" />
<PackageReference Include="YDotNet.Server" Version="0.4.3" />

21
backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs

@ -48,21 +48,16 @@ public sealed class DefaultDomainObjectCache : IDomainObjectCache
return typed;
}
var buffer = await distributedCache.GetAsync(cacheKey, ct);
if (buffer == null)
var cached = await distributedCache.GetAsync(cacheKey, ct);
if (cached == null)
{
return default!;
}
try
{
using (var stream = new MemoryStream(buffer))
{
var result = serializer.Deserialize<T>(stream);
return result;
}
using var stream = new MemoryStream(cached);
return serializer.Deserialize<T>(stream);
}
catch
{
@ -83,12 +78,10 @@ public sealed class DefaultDomainObjectCache : IDomainObjectCache
cache.Set(cacheKey, snapshot, cacheOptions.AbsoluteExpirationRelativeToNow!.Value);
try
{
using (var stream = DefaultPools.MemoryStream.GetStream())
{
serializer.Serialize(snapshot, stream);
using var stream = DefaultPools.MemoryStream.GetStream();
serializer.Serialize(snapshot, stream);
await distributedCache.SetAsync(cacheKey, stream.ToArray(), cacheOptions, ct);
}
await distributedCache.SetAsync(cacheKey, stream.ToArray(), cacheOptions, ct);
}
catch
{

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

@ -98,7 +98,7 @@ public sealed record FilterSchema(FilterSchemaType Type)
}
else
{
return Enumerable.Empty<FilterField>();
return [];
}
}).SelectMany(x => x);

14
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -24,13 +24,13 @@
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="7.25.0" />
<PackageReference Include="Squidex.Caching" Version="7.25.0" />
<PackageReference Include="Squidex.Events" Version="7.25.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.25.0" />
<PackageReference Include="Squidex.Log" Version="7.25.0" />
<PackageReference Include="Squidex.Messaging" Version="7.25.0" />
<PackageReference Include="Squidex.Text" Version="7.25.0" />
<PackageReference Include="Squidex.Assets" Version="7.26.0" />
<PackageReference Include="Squidex.Caching" Version="7.26.0" />
<PackageReference Include="Squidex.Events" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.26.0" />
<PackageReference Include="Squidex.Log" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging" Version="7.26.0" />
<PackageReference Include="Squidex.Text" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

2
backend/src/Squidex.Web/Services/StringLocalizer.cs

@ -82,7 +82,7 @@ public sealed class StringLocalizer : IStringLocalizer, IStringLocalizerFactory
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
return Enumerable.Empty<LocalizedString>();
return [];
}
public IStringLocalizer WithCulture(CultureInfo culture)

2
backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs

@ -43,7 +43,7 @@ public sealed class ErrorDtoProcessor : IOperationProcessor
.OfType<XElement>()
.Where(x => x.Name == "response")
.Where(x => x.Attribute("code") != null)
?? Enumerable.Empty<XElement>();
?? [];
foreach (var response in responses)
{

4
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs

@ -59,8 +59,8 @@ public sealed class SchemasOpenApiGenerator(
var context =
new DocumentProcessorContext(document,
Enumerable.Empty<Type>(),
Enumerable.Empty<Type>(),
[],
[],
schemaResolver,
openApiGenerator,
openApiSettings);

29
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/GenerateSchemaDto.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Schemas.Models;
public sealed class GenerateSchemaDto
{
/// <summary>
/// The prompt to generate.
/// </summary>
[LocalizedRequired]
public string Prompt { get; set; }
/// <summary>
/// Indicates if the schema should actually be generated.
/// </summary>
public bool Execute { get; set; }
/// <summary>
/// The number of content items to generate.
/// </summary>
public int NumberOfContentItems { get; set; }
}

30
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/GenerateSchemaResponseDto.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models;
public sealed class GenerateSchemaResponseDto
{
/// <summary>
/// The status log.
/// </summary>
public ReadonlyList<string> Log { get; set; } = [];
/// <summary>
/// The name of the created schema.
/// </summary>
public string? SchemaName { get; set; }
public static GenerateSchemaResponseDto FromDomain(SchemaAIResult response)
{
return SimpleMapper.Map(response, new GenerateSchemaResponseDto());
}
}

30
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core.GenerateFilters;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
@ -31,6 +32,7 @@ public sealed class SchemasController(
ICommandBus commandBus,
IContentWorkflow workflow,
IAppProvider appProvider,
SchemaAIGenerator schemaAIGenerator,
ScriptingCompleter scriptingCompleter)
: ApiController(commandBus)
{
@ -105,6 +107,34 @@ public sealed class SchemasController(
return CreatedAtAction(nameof(GetSchema), new { app, schema = request.Name }, response);
}
/// <summary>
/// Generate a new schema.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The schema object that needs to be added to the app.</param>
/// <response code="201">Schema created.</response>
/// <response code="400">Schema request not valid.</response>
/// <response code="409">Schema name already in use.</response>
[HttpPost]
[Route("apps/{app}/schemas/generate")]
[ProducesResponseType(typeof(GenerateSchemaResponseDto), StatusCodes.Status201Created)]
[ApiPermissionOrAnonymous(PermissionIds.AppSchemasCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostSchemaGenerate(string app, [FromBody] GenerateSchemaDto request)
{
var result =
await schemaAIGenerator.ExecuteAsync(
App,
request.Prompt,
request.NumberOfContentItems,
request.Execute,
HttpContext.RequestAborted);
var response = GenerateSchemaResponseDto.FromDomain(result);
return Ok(response);
}
/// <summary>
/// Update a schema.
/// </summary>

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

@ -9,8 +9,9 @@ namespace Squidex.Config.Domain;
public static class ConfigurationExtensions
{
public static void ConfigureForSquidex(this IConfigurationBuilder builder)
public static void ConfigureForSquidex(this IConfigurationBuilder builder, IHostEnvironment environment)
{
builder.AddJsonFile("appsettings.Custom.json", true);
builder.AddKeyPerFile(Path.Combine(environment.ContentRootPath, "Configuration"), true, true);
}
}

10
backend/src/Squidex/Config/Domain/ContentsServices.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.CLI.Commands.Implementation.AI;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Templates;
@ -103,5 +104,14 @@ public static class ContentsServices
services.AddSingletonAs<TemplatesClient>()
.AsSelf();
services.AddSingletonAs<SessionFactory>()
.AsSelf();
services.AddSingletonAs<SchemaAIGenerator>()
.AsSelf();
services.AddSingletonAs<AIQueryCache>()
.As<IQueryCache>();
}
}

62
backend/src/Squidex/Config/Startup/LogConfigurationHost.cs

@ -5,35 +5,34 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.RegularExpressions;
using Squidex.Log;
namespace Squidex.Config.Startup;
public sealed class LogConfigurationHost(IConfiguration configuration, ISemanticLog log) : IHostedService
{
private const int MaxValueLength = 30;
private static readonly string RedactedValue = "*****";
private static readonly Regex[] SensitivePatterns =
private static readonly string[] SensitiveValues =
[
// Authentication and API keys
#pragma warning disable MA0110 // Use the Regex source generator
new Regex(@"(?i)(secret|token|key|password|credential|auth|api[_-]?key)$"),
new Regex(@"(?i)^(aws|azure|google|microsoft|github)[_-]"),
new Regex(@"(?i)(jwt|bearer|oauth|saml)"),
new Regex(@"(?i)(client|secret|password)$"),
// Connection strings and credentials
new Regex(@"(?i)(connectionstring|connection)$"),
new Regex(@"(?i)(username|password|credential)$"),
// Cloud provider specific
new Regex(@"(?i)(accesskey|secretkey|privatekey|publickey)$"),
new Regex(@"(?i)(projectid|tenantid)$"),
// Database specific
new Regex(@"(?i)(mongodb|sqlserver|postgres|mysql)://.*"),
new Regex(@"(?i)(database|db|server|host|port|user|pass)="),
#pragma warning restore MA0110 // Use the Regex source generator
"aws",
"azure",
"bearer",
"clientid",
"credential",
"database",
"db",
"github",
"google",
"jwt",
"key",
"microsoft",
"pass",
"secret",
"server",
"tenant",
"token",
"username",
];
public Task StartAsync(
@ -56,14 +55,23 @@ public sealed class LogConfigurationHost(IConfiguration configuration, ISemantic
continue;
}
var keyLower = key.ToLowerInvariant();
if (logged.Add(keyLower))
var lowerKey = key.ToLowerInvariant();
if (!logged.Add(lowerKey))
{
var formattedValue = IsSensitiveKey(keyLower) || IsSensitiveValue(value) ? RedactedValue : value;
continue;
}
c.WriteProperty(keyLower, value);
var formattedValue = value;
if (IsSensitiveKey(lowerKey) || IsSensitiveKey(value) || IsSensitiveValue(value))
{
formattedValue = RedactedValue;
}
else if (formattedValue.Length > MaxValueLength)
{
formattedValue = formattedValue[.. (MaxValueLength - 3)] + "...";
}
c.WriteProperty(lowerKey, formattedValue);
}
}));
@ -72,7 +80,7 @@ public sealed class LogConfigurationHost(IConfiguration configuration, ISemantic
private static bool IsSensitiveKey(string key)
{
return SensitivePatterns.Any(pattern => pattern.IsMatch(key));
return SensitiveValues.Any(pattern => key.Contains(pattern, StringComparison.OrdinalIgnoreCase));
}
private static bool IsSensitiveValue(string? value)

84
backend/src/Squidex/Configuration/schemas__generateprompt

@ -0,0 +1,84 @@
You are a agent to create sample content for a headless CMS.
When asked to create a **schema** for a content type, return a JSON document as the **first markdown code block**. Use the following format:
```
{
"name": string, // The schema name in kebab-case,
"hint": string | null | undefined // Optional description of the schema,
"fields": [{
"name": string, // Field name in camelCase,
"hint": string | null | undefined // Optional description of the field,
"type": "Slug | Text | MultilineText | Markdown | Number | Boolean",
"isRequired": false,
"isLocalized": false,
"minLength": number | null, // For Slug, Text, MultilineText, Markdown
"maxLength" number | null, // For Slug, Text, MultilineText, Markdown
"minValue": number | null, // For Number
"maxValue": number | null, // For Number
}]
}
```
### 🚫 Absolutely Forbidden Fields
Do **not** include any **CMS metadata fields** under any circumstances. These fields are automatically managed by the CMS system and **not authored by content creators**. This includes fields related to:
- **Publishing state** (e.g., `publishedDate`, `isPublished`, `published`)
- **Workflow state** (e.g., `isDraft`, `status`)
- **System timestamps** (e.g., `createdAt`, `updatedAt`)
- **Visibility** (e.g., `visibility`, `ready`)
Here are some **examples** of prohibited fields:
- `createdAt`
- `isDraft`
- `isReadyForPublish`
- `isPublished`
- `publishDate`
- `published`
- `publishedDate`
- `ready`
- `status`
- `updatedAt`
- `visibility`
⚠️ **Note:** This list is *not exhaustive*. Any field related to **system-level behavior, versioning, publishing workflow, or timestamps** should **never** be included.
Only include **fields that describe the actual content** of the schema.
### Schema Rules:
1. Use **kebab-case** for the schema name (e.g., my-schema).
2. Use **camelCase** for field names (e.g., myField).
3. Ensure that the schema **`hint`** accurately reflects the purpose of the schema, but keep it short and precise. Do not just repeat the fields.
4. Ensure that the field **hint** accurately reflects the purpose of the schema. It can be omitted if it does not provide useful or necessary information
When asked to create **sample content**, return a second **markdown code block** with valid JSON—an **array of objects**.
### Sample Content Rules:
1. The result **must be valid JSON** representing an array of objects.
2. Each object's properties must match the **field names** in the schema.
3. Keep the **size of the sample content reasonable**.
4. For **localized fields**, use the following format (only use the language codes specified in the request):
```
{
"languageCode": "Hello World"
}
```
5. For **image fields**, use this structure:
```
{
"fileName": "Example.png"
"description": "Describe the image."
}
```
### 🛠 Correction Instructions:
If you are asked to correct errors, **always return the full response**, including both schema and sample content (if requested). Do not omit the schema, even if it was already correct.

2
backend/src/Squidex/Program.cs

@ -25,7 +25,7 @@ public static class Program
})
.ConfigureAppConfiguration((hostContext, builder) =>
{
builder.ConfigureForSquidex();
builder.ConfigureForSquidex(hostContext.HostingEnvironment);
})
.ConfigureServices((context, services) =>
{

33
backend/src/Squidex/Squidex.csproj

@ -45,6 +45,7 @@
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.RulesetToEditorconfigConverter" Version="3.3.3" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="8.0.18" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.2.1" />
@ -59,17 +60,17 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.4.1" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.S3" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.25.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="21.7.0" />
<PackageReference Include="Squidex.Events.GetEventStore" Version="7.25.0" />
<PackageReference Include="Squidex.Hosting" Version="7.25.0" />
<PackageReference Include="Squidex.Messaging.All" Version="7.25.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.S3" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.26.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="21.8.0" />
<PackageReference Include="Squidex.Events.GetEventStore" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.All" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.4.3" />
<PackageReference Include="YDotNet.Native" Version="0.4.3" />
@ -83,11 +84,11 @@
</ItemGroup>
<ItemGroup Condition="'$(IncludeMagick)' == 'true'">
<PackageReference Include="Squidex.Assets.ImageMagick" Version="7.25.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="7.26.0" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeKafka)' == 'true'">
<PackageReference Include="Squidex.Messaging.Kafka" Version="7.25.0" />
<PackageReference Include="Squidex.Messaging.Kafka" Version="7.26.0" />
</ItemGroup>
<PropertyGroup>
@ -135,6 +136,12 @@
<Folder Include="Areas\Frontend\Resources\" />
</ItemGroup>
<ItemGroup>
<Content Include="Configuration\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\..\i18n\frontend_en.json" Link="Areas\Frontend\Resources\frontend_en.json" />
<EmbeddedResource Include="..\..\i18n\frontend_it.json" Link="Areas\Frontend\Resources\frontend_it.json" />

10
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs

@ -167,7 +167,7 @@ public class ValueConvertersTests
var actual =
new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>());
.ConvertItemAfter(field, source, []);
var expected =
JsonValue.Object()
@ -196,7 +196,7 @@ public class ValueConvertersTests
var actual =
new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>());
.ConvertItemAfter(field, source, []);
var expected = source;
@ -221,7 +221,7 @@ public class ValueConvertersTests
var actual =
new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>());
.ConvertItemAfter(field, source, []);
var expected = source;
@ -245,7 +245,7 @@ public class ValueConvertersTests
var actual =
new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>());
.ConvertItemAfter(field, source, []);
var expected = source;
@ -265,7 +265,7 @@ public class ValueConvertersTests
var actual =
new AddSchemaNames(ResolvedComponents.Empty)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>());
.ConvertItemAfter(field, source, []);
var expected = source;

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs

@ -27,7 +27,7 @@ public class SubscriptionPublisherTests
public SubscriptionPublisherTests()
{
sut = new SubscriptionPublisher(subscriptionService, Enumerable.Empty<ISubscriptionEventCreator>());
sut = new SubscriptionPublisher(subscriptionService, []);
}
[Fact]

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs

@ -58,7 +58,7 @@ public class ConfigPlansProviderTests
[InlineData("my-plan")]
public void Should_return_infinite_if_nothing_configured(string? planId)
{
var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>());
var sut = new ConfigPlansProvider([]);
var actual = sut.GetActualPlan(planId);
@ -78,7 +78,7 @@ public class ConfigPlansProviderTests
[Fact]
public void Should_return_infinite_plan_for_free_plan_if_not_found()
{
var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>());
var sut = new ConfigPlansProvider([]);
var plan = sut.GetFreePlan();

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs

@ -101,7 +101,7 @@ public class ContentEnricherTests : GivenContext
{
var source = CreateContent();
var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), AppProvider);
var sut = new ContentEnricher([], AppProvider);
var actual = await sut.EnrichAsync(source, true, ApiContext, CancellationToken);
@ -113,7 +113,7 @@ public class ContentEnricherTests : GivenContext
{
var source = CreateContent();
var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), AppProvider);
var sut = new ContentEnricher([], AppProvider);
var actual = await sut.EnrichAsync(source, false, ApiContext, CancellationToken);

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs

@ -26,7 +26,7 @@ public abstract class HandlerTestBase<TState> : GivenContext
get => persistenceFactory;
}
public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = Enumerable.Empty<Envelope<IEvent>>();
public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = [];
protected HandlerTestBase()
{
@ -41,7 +41,7 @@ public abstract class HandlerTestBase<TState> : GivenContext
.Invokes((IReadOnlyList<Envelope<IEvent>> events, CancellationToken _) => LastEvents = events);
A.CallTo(() => persistence.DeleteAsync(CancellationToken))
.Invokes(() => LastEvents = Enumerable.Empty<Envelope<IEvent>>());
.Invokes(() => LastEvents = []);
#pragma warning restore MA0056 // Do not call overridable members in constructor
}

16
frontend/.storybook/main.js

@ -8,17 +8,15 @@
const CopyPlugin = require('copy-webpack-plugin');
class FilterSassWarningsPlugin {
apply(compiler) {
compiler.hooks.done.tap('FilterSassWarningsPlugin', (stats) => {
stats.compilation.warnings = stats.compilation.warnings.filter(
(warning) => {
const message = warning.message || warning.toString();
return !message.includes('sass-loader');
}
);
apply(compiler) {
compiler.hooks.done.tap('FilterSassWarningsPlugin', (stats) => {
stats.compilation.warnings = stats.compilation.warnings.filter(warning => {
const message = warning.message || warning.toString();
return !message.includes('sass-loader');
});
}
});
}
}
module.exports = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],

2
frontend/src/app/features/apps/pages/apps-page.component.html

@ -41,7 +41,7 @@
</div>
} @empty {
<div class="empty">
<h3 class="empty-headline">{{ "apps.empty" | sqxTranslate }}</h3>
<h5 class="empty-headline">{{ "apps.empty" | sqxTranslate }}</h5>
</div>
}
</div>

2
frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html

@ -47,7 +47,7 @@
</div>
<div class="form-group">
<sqx-control-errors for="name" />
<sqx-control-errors for="name" [submitCount]="addFieldForm.submitCount | async" />
<input
class="form-control"
#nameInput

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

@ -1,116 +1,169 @@
<form [formGroup]="createForm.form" (ngSubmit)="createSchema()">
<sqx-modal-dialog (dialogClose)="emitClose()" tourId="schemaForm">
<form [formGroup]="actualForm.form" (ngSubmit)="createSchema()">
<sqx-modal-dialog (dialogClose)="emitClose()" hasTabs="true" size="lg" tourId="schemaForm">
<ng-container title>
@if (import) {
@if (source) {
{{ "schemas.clone" | sqxTranslate }}
} @else {
{{ "schemas.create" | sqxTranslate }}
}
</ng-container>
<ng-container content>
<sqx-form-error [error]="createForm.error | async" />
<div class="form-group">
<label for="name">
{{ "common.name" | sqxTranslate }} <small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small>
</label>
<sqx-control-errors for="name" />
<input class="form-control" id="name" autocomplete="off" formControlName="name" sqxFocusOnInit sqxTransformInput="LowerCase" />
<sqx-form-hint> {{ "schemas.schemaNameHint" | sqxTranslate }} </sqx-form-hint>
<ng-container tabs>
<div class="row align-items-center">
<div class="col">
<ul class="nav nav-tabs2">
<li class="nav-item">
<a class="nav-link" [class.active]="selectedTab === 0" (click)="selectTab(0)">
{{ "schemas.createCustom" | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" [class.active]="selectedTab === 1" (click)="selectTab(1)">
{{ "schemas.createFromJson" | sqxTranslate }}
</a>
</li>
@if (hasChatBot) {
<li class="nav-item">
<a class="nav-link" [class.active]="selectedTab === 2" (click)="selectTab(2)">
{{ "schemas.createAI" | sqxTranslate }}
<span class="badge rounded-pill badge-primary">New</span>
</a>
</li>
}
</ul>
</div>
</div>
</ng-container>
<ng-container content>
<sqx-form-error [error]="actualForm.error | async" />
@if (selectedTab !== 2) {
<div class="form-group">
<label for="name">
{{ "common.name" | sqxTranslate }} <small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small>
</label>
<sqx-control-errors for="name" [submitCount]="createForm.submitCount | async" />
<input class="form-control" id="name" autocomplete="off" formControlName="name" sqxFocusOnInit sqxTransformInput="LowerCase" />
<sqx-form-hint> {{ "schemas.schemaNameHint" | sqxTranslate }} </sqx-form-hint>
</div>
<sqx-form-alert> {{ "schemas.nameWarning" | sqxTranslate }} </sqx-form-alert>
}
<div class="form-group mt-4">
<div class="row">
<div class="col-6 type">
<label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Default'" />
<div class="row g-0">
<div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Default'">
<i class="icon-multiple-content"></i>
@if (selectedTab === 0) {
<div class="form-group mt-4">
<div class="row">
<div class="col-12 col-md-4 type">
<label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Default'" />
<div class="row flex-nowrap g-0">
<div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Default'">
<i class="icon-multiple-content"></i>
</div>
</div>
</div>
<div class="col">
<div class="type-title">{{ "schemas.modeMultiple" | sqxTranslate }}</div>
<div class="col">
<div class="type-title">{{ "schemas.modeMultiple" | sqxTranslate }}</div>
<div class="type-text text-muted">{{ "schemas.modeMultipleDescription" | sqxTranslate }}</div>
<div class="type-text text-muted">{{ "schemas.modeMultipleDescription" | sqxTranslate }}</div>
</div>
</div>
</div>
</label>
</div>
</label>
</div>
<div class="col-6 type">
<label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Singleton'" />
<div class="row g-0">
<div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Singleton'">
<i class="icon-single-content"></i>
<div class="col-12 col-md-4 type">
<label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Singleton'" />
<div class="row flex-nowrap g-0">
<div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Singleton'">
<i class="icon-single-content"></i>
</div>
</div>
</div>
<div class="col">
<div class="type-title">{{ "schemas.modeSingle" | sqxTranslate }}</div>
<div class="col">
<div class="type-title">{{ "schemas.modeSingle" | sqxTranslate }}</div>
<div class="type-text text-muted">{{ "schemas.modeSingleDescription" | sqxTranslate }}</div>
<div class="type-text text-muted">{{ "schemas.modeSingleDescription" | sqxTranslate }}</div>
</div>
</div>
</div>
</label>
</div>
</label>
</div>
<div class="col-6 type">
<label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Component'" />
<div class="row g-0">
<div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Component'">
<i class="icon-component"></i>
<div class="col-12 col-md-4 type">
<label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Component'" />
<div class="row flex-nowrap g-0">
<div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Component'">
<i class="icon-component"></i>
</div>
</div>
</div>
<div class="col">
<div class="type-title">{{ "schemas.modeComponent" | sqxTranslate }}</div>
<div class="col">
<div class="type-title">{{ "schemas.modeComponent" | sqxTranslate }}</div>
<div class="type-text text-muted">{{ "schemas.modeComponentDescription" | sqxTranslate }}</div>
<div class="type-text text-muted">{{ "schemas.modeComponentDescription" | sqxTranslate }}</div>
</div>
</div>
</div>
</label>
</label>
</div>
</div>
</div>
</div>
<sqx-form-alert> {{ "schemas.nameWarning" | sqxTranslate }} </sqx-form-alert>
@if (schemasState.categoryNames | async; as categories) {
@if (categories.length > 0) {
<div class="form-group">
<label for="category">{{ "common.category" | sqxTranslate }}</label>
<select class="form-select" id="category" formControlName="initialCategory">
<option></option>
@for (category of categories; track category) {
<option [ngValue]="category">{{ category }}</option>
}
</select>
</div>
}
}
<div class="form-group">
<button class="btn btn-sm btn-text-secondary" [class.hidden]="showImport" (click)="toggleImport()" type="button">
{{ "schemas.import" | sqxTranslate }}
</button>
<button class="btn btn-sm btn-text-secondary force" [class.hidden]="!showImport" (click)="toggleImport()" type="button">
{{ "common.hide" | sqxTranslate }}
</button>
@if (showImport) {
<sqx-code-editor formControlName="importing" [height]="250" valueMode="Json" />
@if (schemasState.categoryNames | async; as categories) {
@if (categories.length > 0) {
<div class="form-group">
<label for="category">{{ "common.category" | sqxTranslate }}</label>
<select class="form-select" id="category" formControlName="initialCategory">
<option></option>
@for (category of categories; track category) {
<option [ngValue]="category">{{ category }}</option>
}
</select>
</div>
}
}
</div>
} @else if (selectedTab === 1) {
<sqx-code-editor formControlName="importing" [height]="1000" valueMode="Json" />
} @else if (selectedTab === 2) {
<div class="row g-2 form-group">
<div class="col">
<label for="prompt">
{{ "common.prompt" | sqxTranslate }} <small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small>
</label>
<sqx-control-errors for="prompt" />
<input class="form-control" id="prompt" autocomplete="off" formControlName="prompt" sqxFocusOnInit />
<sqx-form-hint> {{ "schemas.promptHint" | sqxTranslate }} </sqx-form-hint>
</div>
<div class="col-auto">
<label>&nbsp;</label>
<div>
<button class="btn btn-primary" (click)="generatePreview()" [disabled]="generateForm.submitting | async" type="button">
@if (generateForm.submitting | async) {
<sqx-loader color="white" size="12" />
}
{{ "common.generate" | sqxTranslate }}
</button>
</div>
</div>
</div>
<sqx-form-alert> {{ "schemas.promptExample" | sqxTranslate }} </sqx-form-alert>
<sqx-code-editor disabled="true" [height]="1000" [ngModel]="generateLog" [ngModelOptions]="{ standalone: true }" />
}
</ng-container>
<ng-container footer>
<button class="btn btn-text-secondary" (click)="dialogClose.emit()" type="button">
{{ "common.cancel" | sqxTranslate }}
</button>
<button class="btn btn-success" type="submit">{{ "common.create" | sqxTranslate }}</button>
<button class="btn btn-success" [disabled]="selectedTab === 2 && !generateLog" type="submit">
{{ "common.create" | sqxTranslate }}
</button>
</ng-container>
</sqx-modal-dialog>
</form>

85
frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts

@ -6,9 +6,10 @@
*/
import { AsyncPipe } from '@angular/common';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ApiUrlConfig, AppsState, CodeEditorComponent, ControlErrorsComponent, CreateSchemaForm, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, ModalDialogComponent, SchemaDto, SchemasState, TooltipDirective, TransformInputDirective, TranslatePipe } from '@app/shared';
import { switchMap } from 'rxjs';
import { ApiUrlConfig, AppsState, CodeEditorComponent, ControlErrorsComponent, CreateSchemaForm, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, GenerateSchemaDto, GenerateSchemaForm, LoaderComponent, ModalDialogComponent, SchemaDto, SchemasService, SchemasState, TooltipDirective, TransformInputDirective, TranslatePipe, UIOptions } from '@app/shared';
@Component({
selector: 'sqx-schema-form',
@ -23,6 +24,7 @@ import { ApiUrlConfig, AppsState, CodeEditorComponent, ControlErrorsComponent, C
FormErrorComponent,
FormHintComponent,
FormsModule,
LoaderComponent,
ModalDialogComponent,
ReactiveFormsModule,
TooltipDirective,
@ -31,6 +33,8 @@ import { ApiUrlConfig, AppsState, CodeEditorComponent, ControlErrorsComponent, C
],
})
export class SchemaFormComponent implements OnInit {
public readonly hasChatBot = inject(UIOptions).value.canUseChatBot;
@Output()
public create = new EventEmitter<SchemaDto>();
@ -38,29 +42,40 @@ export class SchemaFormComponent implements OnInit {
public dialogClose = new EventEmitter();
@Input()
public import: any;
public source: any;
public createForm = new CreateSchemaForm();
public generateForm = new GenerateSchemaForm();
public generateLog: string = '';
public selectedTab = 0;
public showImport = false;
public get actualForm() {
return this.selectedTab === 2 ? this.generateForm : this.createForm;
}
constructor(
public readonly apiUrl: ApiUrlConfig,
public readonly appsState: AppsState,
public readonly schemasState: SchemasState,
private readonly schemasService: SchemasService,
) {
}
public ngOnInit() {
this.createForm.load({ type: 'Default', ...this.import, name: '' });
this.createForm.load({ type: 'Default', ...this.source, name: '' });
this.showImport = !!this.import;
if (this.source) {
this.selectedTab = 1;
}
}
public toggleImport() {
this.showImport = !this.showImport;
public selectTab(tab: number) {
this.selectedTab = tab;
this.source = null;
return false;
const { type, name } = this.createForm.form.value;
this.createForm.load({ type, name });
}
public emitCreate(value: SchemaDto) {
@ -72,6 +87,58 @@ export class SchemaFormComponent implements OnInit {
}
public createSchema() {
if (this.selectedTab === 2) {
this.generateCore();
} else {
this.createCore();
}
}
public generatePreview() {
const value = this.generateForm.submit();
if (!value) {
return;
}
this.generateLog = '';
const dto = new GenerateSchemaDto({ ...value, numberOfContentItems: 20, execute: false });
this.schemasService.generateSchema(this.appsState.appName, dto)
.subscribe({
next: dto => {
this.generateLog = dto.log.join('\n');
this.generateForm.submitCompleted({ noReset: true });
},
error: error => {
this.generateForm.submitFailed(error);
},
});
}
private generateCore() {
const value = this.generateForm.submit();
if (!value || !this.generateLog) {
return;
}
const dto = new GenerateSchemaDto({ ...value, numberOfContentItems: 20, execute: true });
this.schemasService.generateSchema(this.appsState.appName, dto)
.pipe(
switchMap(p => this.schemasService.getSchema(this.appsState.appName, p.schemaName!)),
)
.subscribe({
next: dto => {
this.schemasState.add(dto);
this.emitCreate(dto);
},
error: error => {
this.createForm.submitFailed(error);
},
});
}
public createCore() {
const value = this.createForm.submit();
if (!value) {
return;

2
frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html

@ -41,5 +41,5 @@
</form>
</ng-container>
</sqx-layout>
<sqx-schema-form (create)="redirectSchema($event)" (dialogClose)="addSchemaDialog.hide()" [import]="import" *sqxModal="addSchemaDialog" />
<sqx-schema-form (create)="redirectSchema($event)" (dialogClose)="addSchemaDialog.hide()" [source]="source" *sqxModal="addSchemaDialog" />
<router-outlet />

8
frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -54,7 +54,7 @@ export class SchemasPageComponent implements OnInit {
return getCategoryTree(schemas, categories, filter);
});
public import: any;
public source: any;
constructor(
public readonly schemasState: SchemasState,
@ -68,7 +68,7 @@ export class SchemasPageComponent implements OnInit {
this.subscriptions.add(
this.messageBus.of(SchemaCloning)
.subscribe(event => {
this.import = event.schema;
this.source = event.schema;
this.addSchemaDialog.show();
}));
@ -105,8 +105,8 @@ export class SchemasPageComponent implements OnInit {
this.addSchemaDialog.hide();
}
public createSchema(importing: any = null) {
this.import = importing;
public createSchema(source: any = null) {
this.source = source;
this.addSchemaDialog.show();
}

8
frontend/src/app/framework/angular/forms/control-errors.component.ts

@ -36,6 +36,9 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
@Input({ required: true })
public for!: string | AbstractControl;
@Input()
public submitCount: null | number | undefined;
@Input()
public fieldName: string | null | undefined;
@ -100,7 +103,10 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
private createMessages() {
const errorMessages: string[] = [];
if (this.control && this.control.invalid && this.isTouched && this.control.errors) {
const submitCount = this.submitCount;
const submitted = !Types.isNumber(submitCount) || submitCount > 0;
if (this.control && this.control.invalid && this.isTouched && this.control.errors && submitted) {
for (const [key, error] of Object.entries(this.control.errors)) {
const message = formatError(this.localizer, this.controlDisplayName, key, error, this.control.value);

5
frontend/src/app/framework/angular/forms/editors/code-editor.component.scss

@ -28,11 +28,6 @@
}
}
.ace_active-line,
.ace_gutter-active-line {
background: none !important;
}
.ace_gutter-active-line {
background: color.adjust($color-border, $lightness: -5%) !important;
}

14
frontend/src/app/framework/angular/forms/editors/code-editor.component.ts

@ -49,6 +49,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
@Input()
public valueMode: 'String' | 'Json' | 'JsonString' = 'String';
@Input({ transform: booleanAttribute })
public disableTools = false;
@Input({ transform: numberAttribute })
public maxLines: number | undefined;
@ -96,7 +99,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
this.setMode();
}
if (changes.height || changes.maxLines || changes.minLines || changes.singleLine || changes.snippets) {
if (changes.disabled || changes.height || changes.maxLines || changes.minLines || changes.singleLine || changes.snippets) {
this.setOptions();
}
@ -166,7 +169,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
this.setWordWrap();
this.onDisabled(this.snapshot.isDisabled);
if (this.aceTools) {
if (this.aceTools && !this.disableTools) {
const originalCompleter = this.aceEditor.completer!;
const originalShowDocTooltip = originalCompleter.showDocTooltip;
originalCompleter.showDocTooltip = item => {
@ -205,9 +208,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
const previous = this.aceEditor.completers || [];
this.aceEditor.completers = [...previous, completer];
}
this.aceEditor.on('blur', () => {
this.changeValue();
this.callTouched();
});
@ -310,8 +313,13 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
minLines,
printMargin: !this.singleLine,
showGutter: !this.singleLine,
useWorker: !this.snapshot.isDisabled,
});
if (this.snapshot.isDisabled) {
this.aceEditor.getSession().setAnnotations([]);
}
this.aceEditor.commands.bindKey('Enter|Shift-Enter', this.singleLine ? 'null' : undefined as any);
}

18
frontend/src/app/framework/angular/forms/editors/code-editor.stories.ts

@ -36,6 +36,7 @@ export default {
[disabled]="disabled"
[height]="height"
[maxLines]="maxLines"
[mode]="mode"
(ngModelChange)="change($event)"
[ngModel]="ngModel"
[singleLine]="singleLine"
@ -85,6 +86,23 @@ export const Completions: Story = {
},
};
export const InvalidJson: Story = {
args: {
height: 200,
ngModel: 'Invalid JSON\n{ \"Hello\": 42 }',
mode: 'ace/mode/javascript',
} as any,
};
export const InvalidJsonWithoutAnnotations: Story = {
args: {
height: 200,
ngModel: 'Invalid JSON\n{ \"Hello\": 42 }',
mode: 'ace/mode/javascript',
disabled: true,
} as any,
};
export const SingleLine: Story = {
render: args => ({
props: args,

3
frontend/src/app/framework/angular/forms/model.ts

@ -91,14 +91,13 @@ export class Form<T extends AbstractControl, TOut, TIn = TOut> {
}
public submit(): TOut | null {
this.updateSubmitState(null, true);
this.form.markAllAsTouched();
if (!hasNonCustomError(this.form)) {
const value = this.transformSubmit(this.form.value);
if (value) {
this.updateSubmitState(null, true);
this.disable();
}

2
frontend/src/app/framework/angular/modals/dialog-renderer.component.html

@ -23,7 +23,7 @@
<div class="notification-container notification-container-bottom-right">
@for (notification of snapshot.notifications; track notification) {
<div class="alert alert-light alert-dismissible border shadow-sm timed" @fade (click)="close(notification)" role="alert">
<button class="btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button>
<button class="btn-sm btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button>
<div class="timer timer-{{ notification.messageType }}"></div>
<div [sqxMarkdown]="notification.message | sqxTranslate"></div>
</div>

1
frontend/src/app/framework/angular/stateful.component.ts

@ -69,7 +69,6 @@ export abstract class StatefulControlComponent<T extends {}, TValue> extends Sta
public setDisabledState(isDisabled: boolean) {
this.next({ isDisabled } as any);
this.onDisabled(this.snapshot.isDisabled);
}

4
frontend/src/app/shared/components/app-form.component.html

@ -30,7 +30,7 @@
<div class="grid" style="--bs-gap: 0.5rem 0.5rem">
<div
class="g-col-6 g-col-xl-4 card card-template card-href"
class="g-col-6 card card-template card-href"
[class.border-primary]="!template"
(click)="selectTemplate()"
data-testid="new-app"
@ -45,7 +45,7 @@
@for (availableTemplate of templates; track availableTemplate) {
<div
class="g-col-6 g-col-xl-4 card card-template card-href"
class="g-col-6 card card-template card-href"
[class.border-primary]="availableTemplate === template"
(click)="selectTemplate(availableTemplate)">
<div class="card-body">

2
frontend/src/app/shared/components/app-form.component.scss

@ -3,7 +3,7 @@
@import 'vars';
.card-image {
width: 30%;
width: 3rem;
}
.card-title {

2
frontend/src/app/shared/components/team-form.component.html

@ -7,7 +7,7 @@
<label for="teamName">
{{ "common.name" | sqxTranslate }} <small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small>
</label>
<sqx-control-errors for="name" />
<sqx-control-errors for="name" [submitCount]="createForm.submitCount | async" />
<input class="form-control" id="name" autocomplete="off" formControlName="name" sqxFocusOnInit sqxTransformInput="LowerCase" />
<sqx-form-hint> {{ "teams.teamNameHint" | sqxTranslate }} </sqx-form-hint>
</div>

153
frontend/src/app/shared/model/generated.ts

@ -5724,6 +5724,154 @@ export interface IUpsertSchemaNestedFieldDto {
readonly properties: FieldPropertiesDto;
}
export class GenerateSchemaResponseDto implements IGenerateSchemaResponseDto {
/** Uses the cache values because the actual object is frozen. */
private readonly cachedValues: { [key: string]: any } = {};
/** The status log. */
readonly log!: string[];
/** The name of the created schema. */
readonly schemaName!: string;
constructor(data?: IGenerateSchemaResponseDto) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data: any) {
if (Array.isArray(_data["log"])) {
(<any>this).log = [] as any;
for (let item of _data["log"])
(<any>this).log!.push(item);
}
(<any>this).schemaName = _data["schemaName"];
this.cleanup(this);
return this;
}
static fromJSON(data: any): GenerateSchemaResponseDto {
const result = new GenerateSchemaResponseDto().init(data);
result.cleanup(this);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
if (Array.isArray(this.log)) {
data["log"] = [];
for (let item of this.log)
data["log"].push(item);
}
data["schemaName"] = this.schemaName;
this.cleanup(data);
return data;
}
protected cleanup(target: any) {
for (var property in target) {
if (target.hasOwnProperty(property)) {
const value = target[property];
if (value === undefined) {
delete target[property];
}
}
}
}
protected compute<T>(key: string, action: () => T): T {
if (!this.cachedValues.hasOwnProperty(key)) {
const value = action();
this.cachedValues[key] = value;
return value;
} else {
return this.cachedValues[key] as any;
}
}
}
export interface IGenerateSchemaResponseDto {
/** The status log. */
readonly log: string[];
/** The name of the created schema. */
readonly schemaName: string;
}
export class GenerateSchemaDto implements IGenerateSchemaDto {
/** Uses the cache values because the actual object is frozen. */
private readonly cachedValues: { [key: string]: any } = {};
/** The prompt to generate. */
readonly prompt!: string;
/** Indicates if the schema should actually be generated. */
readonly execute!: boolean;
/** The number of content items to generate. */
readonly numberOfContentItems!: number;
constructor(data?: IGenerateSchemaDto) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data: any) {
(<any>this).prompt = _data["prompt"];
(<any>this).execute = _data["execute"];
(<any>this).numberOfContentItems = _data["numberOfContentItems"];
this.cleanup(this);
return this;
}
static fromJSON(data: any): GenerateSchemaDto {
const result = new GenerateSchemaDto().init(data);
result.cleanup(this);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["prompt"] = this.prompt;
data["execute"] = this.execute;
data["numberOfContentItems"] = this.numberOfContentItems;
this.cleanup(data);
return data;
}
protected cleanup(target: any) {
for (var property in target) {
if (target.hasOwnProperty(property)) {
const value = target[property];
if (value === undefined) {
delete target[property];
}
}
}
}
protected compute<T>(key: string, action: () => T): T {
if (!this.cachedValues.hasOwnProperty(key)) {
const value = action();
this.cachedValues[key] = value;
return value;
} else {
return this.cachedValues[key] as any;
}
}
}
export interface IGenerateSchemaDto {
/** The prompt to generate. */
readonly prompt: string;
/** Indicates if the schema should actually be generated. */
readonly execute: boolean;
/** The number of content items to generate. */
readonly numberOfContentItems: number;
}
export class UpdateSchemaDto implements IUpdateSchemaDto {
/** Uses the cache values because the actual object is frozen. */
private readonly cachedValues: { [key: string]: any } = {};
@ -9811,14 +9959,15 @@ export interface IFlowExecutionStepStateDto {
readonly attempts: FlowExecutionStepAttemptDto[];
}
export type FlowExecutionStatus = "Pending" | "Scheduled" | "Completed" | "Failed" | "Running";
export type FlowExecutionStatus = "Pending" | "Scheduled" | "Completed" | "Failed" | "Running" | "Cancelled";
export const FlowExecutionStatusValues: ReadonlyArray<FlowExecutionStatus> = [
"Pending",
"Scheduled",
"Completed",
"Failed",
"Running"
"Running",
"Cancelled"
];
export class FlowExecutionStepAttemptDto implements IFlowExecutionStepAttemptDto {

21
frontend/src/app/shared/services/schemas.service.spec.ts

@ -8,7 +8,7 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { AddFieldDto, ApiUrlConfig, ArrayFieldPropertiesDto, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ChangeCategoryDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, ConfigureFieldRulesDto, ConfigureUIFieldsDto, createProperties, CreateSchemaDto, DateTime, DateTimeFieldPropertiesDto, FieldDto, FieldRuleDto, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NestedFieldDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, Resource, ResourceLinkDto, SchemaDto, SchemaPropertiesDto, SchemaScriptsDto, SchemasDto, SchemasService, ScriptCompletions, StringFieldPropertiesDto, SynchronizeSchemaDto, TagsFieldPropertiesDto, UpdateFieldDto, UpdateSchemaDto, VersionTag } from '@app/shared/internal';
import { AddFieldDto, ApiUrlConfig, ArrayFieldPropertiesDto, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ChangeCategoryDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, ConfigureFieldRulesDto, ConfigureUIFieldsDto, createProperties, CreateSchemaDto, DateTime, DateTimeFieldPropertiesDto, FieldDto, FieldRuleDto, GenerateSchemaDto, GenerateSchemaResponseDto, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NestedFieldDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, Resource, ResourceLinkDto, SchemaDto, SchemaPropertiesDto, SchemaScriptsDto, SchemasDto, SchemasService, ScriptCompletions, StringFieldPropertiesDto, SynchronizeSchemaDto, TagsFieldPropertiesDto, UpdateFieldDto, UpdateSchemaDto, VersionTag } from '@app/shared/internal';
describe('SchemasService', () => {
const version = new VersionTag('1');
@ -104,6 +104,25 @@ describe('SchemasService', () => {
expect(schema!).toEqual(createSchema(12));
}));
it('should make post request to generate schema',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new GenerateSchemaDto({ prompt: 'prompt', execute: true, numberOfContentItems: 1 });
let schema: GenerateSchemaResponseDto;
schemasService.generateSchema('my-app', dto).subscribe(result => {
schema = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/generate');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ log: [], schemaName: 'schema' });
expect(schema!).toEqual(new GenerateSchemaResponseDto({ log: [], schemaName: 'schema' }));
}));
it('should make put request to update schema',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateSchemaDto({ label: 'label1' });

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

@ -10,7 +10,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiUrlConfig, HTTP, pretifyError, Resource, ScriptCompletions, Versioned, VersionOrTag } from '@app/framework';
import { AddFieldDto, ChangeCategoryDto, ConfigureFieldRulesDto, ConfigureUIFieldsDto, CreateSchemaDto, SchemaDto, SchemasDto, SynchronizeSchemaDto, UpdateFieldDto, UpdateSchemaDto } from './../model';
import { AddFieldDto, ChangeCategoryDto, ConfigureFieldRulesDto, ConfigureUIFieldsDto, CreateSchemaDto, GenerateSchemaDto, GenerateSchemaResponseDto, SchemaDto, SchemasDto, SynchronizeSchemaDto, UpdateFieldDto, UpdateSchemaDto } from './../model';
import { QueryModel } from './query';
@Injectable({
@ -53,6 +53,16 @@ export class SchemasService {
pretifyError('i18n:schemas.createFailed'));
}
public generateSchema(appName: string, dto: GenerateSchemaDto): Observable<GenerateSchemaResponseDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/generate`);
return HTTP.postVersioned(this.http, url, dto.toJSON()).pipe(
map(({ payload }) => {
return GenerateSchemaResponseDto.fromJSON(payload.body);
}),
pretifyError('i18n:schemas.createFailed'));
}
public putScripts(appName: string, resource: Resource, dto: Record<string, string>, version: VersionOrTag): Observable<SchemaDto> {
const link = resource._links['update/scripts'];

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

@ -53,7 +53,17 @@ export class CreateSchemaForm extends Form<ExtendedFormGroup, CreateSchemaDto> {
public transformSubmit(value: any) {
const { name, type, importing, initialCategory } = value;
return new CreateSchemaDto({ name, type, category: initialCategory, ...importing });
return CreateSchemaDto.fromJSON({ name, type, category: initialCategory, ...importing });
}
}
export class GenerateSchemaForm extends Form<ExtendedFormGroup, { prompt: string }> {
constructor() {
super(new ExtendedFormGroup({
prompt: new UntypedFormControl('', [
Validators.required,
]),
}));
}
}

8
frontend/src/app/shared/state/schemas.state.ts

@ -360,6 +360,14 @@ export class SchemasState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public add(schema: SchemaDto) {
this.next(s => {
const schemas = [...s.schemas, schema].sortByString(x => x.displayName);
return { ...s, schemas };
}, 'Created');
}
private replaceSchema(schema: SchemaDto, oldVersion?: Version, updateText?: string) {
if (!oldVersion || oldVersion !== schema.version) {
if (updateText) {

6
frontend/src/app/theme/_bootstrap.scss

@ -75,6 +75,12 @@
ul {
margin: 0;
}
&-dismissible {
.btn-close {
@include absolute(.25rem, .25rem, auto, auto);
}
}
}
.alert-hint {

7
frontend/src/app/theme/_forms.scss

@ -85,9 +85,12 @@
}
&-close {
@include absolute(0, 0, auto, auto);
display: none;
@include absolute(.125rem, .125rem, auto, auto);
padding: .5rem;
& {
display: none;
}
}
&-error {

5
tools/.editorconfig

@ -58,7 +58,7 @@ dotnet_diagnostic.MA0004.severity = none
dotnet_diagnostic.MA0006.severity = none
# MA0007: Add a comma after the last value
dotnet_diagnostic.MA0007.severity = none
dotnet_diagnostic.MA0007.severity = warning
# MA0008: Add StructLayoutAttribute
dotnet_diagnostic.MA0008.severity = none
@ -183,5 +183,8 @@ dotnet_diagnostic.SA1615.severity = none
# SA1623: Property summary documentation should match accessors
dotnet_diagnostic.SA1623.severity = none
# SYSLIB1045: Convert to 'GeneratedRegexAttribute'.
dotnet_diagnostic.SYSLIB1045.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

6
tools/TestSuite/TestSuite.ApiTests/AnonymousTests.cs

@ -33,7 +33,7 @@ public class AnonymousTests(ClientFixture fixture) : IClassFixture<ClientFixture
// STEP 2: Make the client anonymous.
var clientRequest = new UpdateClientDto
{
AllowAnonymous = true
AllowAnonymous = true,
};
await app.Apps.PutClientAsync("default", clientRequest);
@ -65,7 +65,7 @@ public class AnonymousTests(ClientFixture fixture) : IClassFixture<ClientFixture
// STEP 2: Make the client anonymous.
var clientRequest = new UpdateClientDto
{
AllowAnonymous = true
AllowAnonymous = true,
};
await app.Apps.PutClientAsync("default", clientRequest);
@ -76,7 +76,7 @@ public class AnonymousTests(ClientFixture fixture) : IClassFixture<ClientFixture
{
Name = "my-content",
// Schema must be published to create content.
IsPublished = true
IsPublished = true,
};
await app.Schemas.PostSchemaAsync(schemaRequest);

4
tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs

@ -57,7 +57,7 @@ public sealed class AppClientsTests(ClientFixture fixture) : IClassFixture<Clien
AllowAnonymous = true,
ApiCallsLimit = 100,
ApiTrafficLimit = 200,
Role = "Owner"
Role = "Owner",
};
var clients_2 = await app.Apps.PutClientAsync(client.Id, updateNameRequest);
@ -99,7 +99,7 @@ public sealed class AppClientsTests(ClientFixture fixture) : IClassFixture<Clien
{
var createRequest = new CreateClientDto
{
Id = id
Id = id,
};
var clients = await app.Apps.PostClientAsync(createRequest);

6
tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs

@ -27,7 +27,7 @@ public sealed class AppContributorsTests(ClientFixture fixture) : IClassFixture<
// STEP 1: Do not invite contributors when flag is false.
var createRequest = new AssignContributorDto
{
ContributorId = "test@squidex.io"
ContributorId = "test@squidex.io",
};
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() =>
@ -72,7 +72,7 @@ public sealed class AppContributorsTests(ClientFixture fixture) : IClassFixture<
{
ContributorId = contributor.ContributorId,
// Test update of role.
Role = "Owner"
Role = "Owner",
};
var contributors_2 = await app.Apps.PostContributorAsync(updateRequest);
@ -132,7 +132,7 @@ public sealed class AppContributorsTests(ClientFixture fixture) : IClassFixture<
// Invite must be true, otherwise new users are not created.
Invite = true,
// The initial role or editor otherwise.
Role = role
Role = role,
};
var contributors = await app.Apps.PostContributorAsync(createInviteRequest);

2
tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs

@ -113,7 +113,7 @@ public class AppCreationTests(ClientFixture fixture) : IClassFixture<ClientFixtu
{
Name = appName,
// The template is just referenced by the name.
Template = template.Name
Template = template.Name,
};
var (app, _) = await _.PostAppAsync(createRequest);

16
tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs

@ -70,9 +70,9 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
{
Fallback =
[
"it"
"it",
],
IsOptional = true
IsOptional = true,
};
var languages_2 = await app.Apps.PutLanguageAsync("de", updateRequest);
@ -101,9 +101,9 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
{
Fallback =
[
"de"
"de",
],
IsOptional = true
IsOptional = true,
};
await app.Apps.PutLanguageAsync("it", updateRequest);
@ -112,7 +112,7 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
// STEP 3: Change master language to Italian.
var masterRequest = new UpdateLanguageDto
{
IsMaster = true
IsMaster = true,
};
var languages_4 = await app.Apps.PutLanguageAsync("it", masterRequest);
@ -150,9 +150,9 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
{
Fallback =
[
"de"
"de",
],
IsOptional = true
IsOptional = true,
};
await app.Apps.PutLanguageAsync("it", updateRequest);
@ -173,7 +173,7 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
{
var createRequest = new AddLanguageDto
{
Language = code
Language = code,
};
await app.Apps.PostLanguageAsync(createRequest);

10
tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs

@ -57,7 +57,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
// STEP 2: Update role..
var updateRequest = new UpdateRoleDto
{
Permissions = ["a", "b"]
Permissions = ["a", "b"],
};
var roles_2 = await _.Client.Apps.PutRoleAsync(roleName, updateRequest);
@ -80,7 +80,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
// STEP 2 Assign client and contributor.
var createClientRequest = new CreateClientDto
{
Id = client
Id = client,
};
await _.Client.Apps.PostClientAsync(createClientRequest);
@ -155,7 +155,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
// Test diffferent role names.
Role = role,
// Invite must be true, otherwise new users are not created.
Invite = true
Invite = true,
};
await _.Client.Apps.PostContributorAsync(assignRequest);
@ -165,7 +165,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
{
var updateRequest = new UpdateClientDto
{
Role = role
Role = role,
};
await _.Client.Apps.PutClientAsync(client, updateRequest);
@ -175,7 +175,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
{
var createRequest = new AddRoleDto
{
Name = name
Name = name,
};
var roles = await _.Client.Apps.PostRoleAsync(createRequest);

10
tools/TestSuite/TestSuite.ApiTests/AppTests.cs

@ -32,7 +32,7 @@ public sealed class AppTests(CreatedAppFixture fixture) : IClassFixture<CreatedA
// STEP 1: Update app.
var updateRequest = new UpdateAppDto
{
Label = Guid.NewGuid().ToString()
Label = Guid.NewGuid().ToString(),
};
var app_1 = await _.Client.Apps.PutAppAsync(updateRequest);
@ -46,7 +46,7 @@ public sealed class AppTests(CreatedAppFixture fixture) : IClassFixture<CreatedA
// STEP 1: Update app.
var updateRequest = new UpdateAppDto
{
Description = Guid.NewGuid().ToString()
Description = Guid.NewGuid().ToString(),
};
var app_1 = await _.Client.Apps.PutAppAsync(updateRequest);
@ -125,12 +125,12 @@ public sealed class AppTests(CreatedAppFixture fixture) : IClassFixture<CreatedA
{
Patterns =
[
new PatternDto { Name = "pattern", Regex = ".*" }
new PatternDto { Name = "pattern", Regex = ".*" },
],
Editors =
[
new EditorDto { Name = "editor", Url = "http://squidex.io/path/to/editor" }
]
new EditorDto { Name = "editor", Url = "http://squidex.io/path/to/editor" },
],
};
var settings_1 = await _.Client.Apps.PutSettingsAsync(updateRequest);

8
tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs

@ -57,12 +57,12 @@ public sealed class AppWorkflowsTests(ClientFixture fixture) : IClassFixture<Cli
{
Transitions = new Dictionary<string, WorkflowTransitionDto>
{
["Published"] = new WorkflowTransitionDto()
}
["Published"] = new WorkflowTransitionDto(),
},
},
["Published"] = new WorkflowStepDto(),
},
Name = workflowName
Name = workflowName,
};
var workflows_2 = await app.Apps.PutWorkflowAsync(workflow.Id, updateRequest);
@ -98,7 +98,7 @@ public sealed class AppWorkflowsTests(ClientFixture fixture) : IClassFixture<Cli
{
var createRequest = new AddWorkflowDto
{
Name = workflowName
Name = workflowName,
};
var workflows = await app.Apps.PostWorkflowAsync(createRequest);

8
tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs

@ -40,7 +40,7 @@ public class AssetFoldersTests(CreatedAppFixture fixture) : IClassFixture<Create
// STEP 2: Update folder
var updateRequest = new RenameAssetFolderDto
{
FolderName = Guid.NewGuid().ToString()
FolderName = Guid.NewGuid().ToString(),
};
var folder_1 = await _.Client.Assets.PutAssetFolderAsync(folder_0.Id, updateRequest);
@ -61,7 +61,7 @@ public class AssetFoldersTests(CreatedAppFixture fixture) : IClassFixture<Create
// STEP 2: Update folder
var moveRequest = new MoveAssetFolderDto
{
ParentId = folder1.Id
ParentId = folder1.Id,
};
var folder2_1 = await _.Client.Assets.PutAssetFolderParentAsync(folder2.Id, moveRequest);
@ -101,7 +101,7 @@ public class AssetFoldersTests(CreatedAppFixture fixture) : IClassFixture<Create
// STEP 2: Update folder
var moveRequest = new MoveAssetFolderDto
{
ParentId = folder2.Id
ParentId = folder2.Id,
};
await Assert.ThrowsAnyAsync<SquidexException>(() => _.Client.Assets.PutAssetFolderParentAsync(folder1.Id, moveRequest));
@ -156,7 +156,7 @@ public class AssetFoldersTests(CreatedAppFixture fixture) : IClassFixture<Create
{
FolderName = name,
// Create a nested asset folder.
ParentId = parentId
ParentId = parentId,
};
var folder = await _.Client.Assets.PostAssetFolderAsync(createRequest);

10
tools/TestSuite/TestSuite.ApiTests/AssetScriptingTests.cs

@ -28,7 +28,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
Create = @"
if (ctx.command.mimeType == 'image/jpeg') {
disallow('We do not use jpeg anymore.');
}"
}",
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -52,7 +52,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
ctx.command.metadata['key1'] = 'value1';
ctx.command.metadata['key2'] = 'value2';
ctx.command.tags.add('tag1');
ctx.command.tags.add('tag2');"
ctx.command.tags.add('tag2');",
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -78,7 +78,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
Update = @"
if (ctx.command.mimeType == 'image/jpeg') {
disallow('We do not use jpeg anymore.');
}"
}",
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -104,7 +104,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
{
Update = @"
ctx.command.metadata['key1'] = 'value1';
ctx.command.metadata['key2'] = 'value2';"
ctx.command.metadata['key2'] = 'value2';",
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -132,7 +132,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
Query = @"
if (ctx.asset.mimeType == 'image/jpeg') {
disallow('We do not use jpeg anymore.');
}"
}",
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);

54
tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -250,8 +250,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Tags =
[
randomTag1,
randomTag2
]
randomTag2,
],
};
await _.Client.Assets.PutAssetAsync(asset_1.Id, randomMetadataRequest, ct);
@ -272,8 +272,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Tags =
[
tag1,
tag2
]
tag2,
],
};
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, metadataRequest);
@ -305,7 +305,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 2: Annotate file name.
var fileNameRequest = new AnnotateAssetDto
{
FileName = "My Image"
FileName = "My Image",
};
await _.Client.Assets.AnnotateAsync(asset_1, fileNameRequest, strategy);
@ -333,8 +333,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Metadata = new Dictionary<string, object>
{
["pw"] = 100L,
["ph"] = 20L
}
["ph"] = 20L,
},
};
await _.Client.Assets.AnnotateAsync(asset_1, metadataRequest, strategy);
@ -359,7 +359,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 2: Annotate slug.
var slugRequest = new AnnotateAssetDto
{
Slug = "my-image"
Slug = "my-image",
};
await _.Client.Assets.AnnotateAsync(asset_1, slugRequest, strategy);
@ -387,8 +387,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Tags =
[
"tag1",
"tag2"
]
"tag2",
],
};
await _.Client.Assets.AnnotateAsync(asset_1, tagsRequest, strategy);
@ -423,7 +423,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 4: Protect asset.
var protectRequest = new AnnotateAssetDto
{
IsProtected = true
IsProtected = true,
};
await _.Client.Assets.AnnotateAsync(asset_1, protectRequest, strategy);
@ -484,7 +484,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 1: Create folder.
var folderRequest = new CreateAssetFolderDto
{
FolderName = "folder"
FolderName = "folder",
};
var folder = await app.Assets.PostAssetFolderAsync(folderRequest);
@ -510,7 +510,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Query = $@"
if (ctx.assetId === '{asset_1.Id}') {{
disallow();
}}"
}}",
};
await app.Apps.PutAssetScriptsAsync(scriptsRequest);
@ -541,7 +541,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 1: Create folder.
var folderRequest = new CreateAssetFolderDto
{
FolderName = "folder"
FolderName = "folder",
};
var folder = await app.Assets.PostAssetFolderAsync(folderRequest);
@ -561,7 +561,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
getAssetBlurHash(ctx.asset, function (hash) {
ctx.command.metadata['blurHash'] = hash;
});
}"
}",
};
await app.Apps.PutAssetScriptsAsync(scriptsRequest);
@ -592,7 +592,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
var assets_1 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery
{
Filter = "metadata/pixelWidth eq 600"
Filter = "metadata/pixelWidth eq 600",
});
Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id);
@ -605,7 +605,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
asset_1.Id,
new AnnotateAssetDto
{
Metadata = asset_1.Metadata
Metadata = asset_1.Metadata,
});
@ -613,7 +613,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
var assets_2 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery
{
Filter = "metadata/custom eq 'foo'"
Filter = "metadata/custom eq 'foo'",
});
Assert.Contains(assets_2.Items, x => x.Id == asset_1.Id);
@ -630,7 +630,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
var assets_1 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery
{
ParentId = Guid.Empty.ToString()
ParentId = Guid.Empty.ToString(),
});
Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id);
@ -642,7 +642,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 1: Create asset folder.
var folderRequest = new CreateAssetFolderDto
{
FolderName = "sub"
FolderName = "sub",
};
var folder = await _.Client.Assets.PostAssetFolderAsync(folderRequest);
@ -656,7 +656,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
var assets_1 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery
{
ParentId = folder.Id
ParentId = folder.Id,
});
Assert.Single(assets_1.Items, x => x.Id == asset_1.Id);
@ -668,7 +668,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 1: Create asset folder.
var createRequest1 = new CreateAssetFolderDto
{
FolderName = "folder1"
FolderName = "folder1",
};
var folder_1 = await _.Client.Assets.PostAssetFolderAsync(createRequest1);
@ -679,7 +679,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
{
FolderName = "subfolder",
// Reference the parent folder by Id, so it must exist first.
ParentId = folder_1.Id
ParentId = folder_1.Id,
};
var folder_2 = await _.Client.Assets.PostAssetFolderAsync(createRequest2);
@ -736,7 +736,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
var deleted = await _.Client.Assets.GetAssetsAsync(
new AssetQuery
{
Filter = "isDeleted eq true"
Filter = "isDeleted eq true",
});
var permanent = strategy is ContentStrategies.Deletion.SinglePermanent or ContentStrategies.Deletion.BulkPermanent;
@ -791,8 +791,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
path = "isDeleted",
op = "eq",
value = true,
}
}
},
},
});
Assert.NotEmpty(assets.Items);
@ -825,7 +825,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 2: Rename tag.
var renameRequest = new RenameTagDto
{
TagName = "pngs"
TagName = "pngs",
};
await client.Assets.PutTagAsync("type/png", renameRequest);

10
tools/TestSuite/TestSuite.ApiTests/BackupTests.cs

@ -62,7 +62,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
{
Url = uri,
// Choose a new app name, because the old one is deleted.
Name = appNameRestore
Name = appNameRestore,
};
await _.Client.Backups.PostRestoreJobAsync(restoreRequest);
@ -115,7 +115,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
{
Url = uri,
// Restore the old app name, because it has been deleted anyway.
Name = appName
Name = appName,
};
await app.Backups.PostRestoreJobAsync(restoreRequest);
@ -137,7 +137,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
await contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
});
@ -155,7 +155,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
// Create a workflow
var workflowRequest = new AddWorkflowDto
{
Name = "workflow"
Name = "workflow",
};
await app.Apps.PostWorkflowAsync(workflowRequest);
@ -164,7 +164,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
// Create a language
var languageRequest = new AddLanguageDto
{
Language = "de"
Language = "de",
};
await app.Apps.PostLanguageAsync(languageRequest);

10
tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs

@ -33,7 +33,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
var content_1 = await contents.CreateAsync(
new TestEntityData
{
String = "hello"
String = "hello",
});
Assert.Equal("hello", content_1.Data.String);
@ -48,7 +48,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
content_1.Id,
new ChangeStatus
{
Status = "Published"
Status = "Published",
});
// Should not return deleted field.
@ -68,7 +68,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
var contentA_1 = await contents.CreateAsync(
new TestEntityWithReferencesData
{
References = null
References = null,
});
@ -76,7 +76,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
var contentB_1 = await contents.CreateAsync(
new TestEntityWithReferencesData
{
References = [contentA_1.Id]
References = [contentA_1.Id],
});
@ -88,7 +88,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
var contentB_2 = await contents.ChangeStatusAsync(contentB_1.Id,
new ChangeStatus
{
Status = "Published"
Status = "Published",
});
// Should not return deleted field.

10
tools/TestSuite/TestSuite.ApiTests/ContentCollationTests.cs

@ -46,10 +46,10 @@ public class ContentCollationTests(CreatedAppFixture fixture) : IClassFixture<Cr
new UpsertSchemaFieldDto
{
Name = SimpleEntityData.StringField,
Properties = new StringFieldPropertiesDto()
Properties = new StringFieldPropertiesDto(),
},
],
IsPublished = true
IsPublished = true,
};
await _.Client.Schemas.PostSchemaAsync(schemaRequest);
@ -60,21 +60,21 @@ public class ContentCollationTests(CreatedAppFixture fixture) : IClassFixture<Cr
await contents.CreateAsync(
new SimpleEntityData
{
String = "İstanbul"
String = "İstanbul",
},
ContentCreateOptions.AsPublish);
await contents.CreateAsync(
new SimpleEntityData
{
String = "Mersin"
String = "Mersin",
},
ContentCreateOptions.AsPublish);
await contents.CreateAsync(
new SimpleEntityData
{
String = "Lüleburgaz"
String = "Lüleburgaz",
},
ContentCreateOptions.AsPublish);

18
tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs

@ -27,8 +27,8 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
Localized = new Dictionary<string, string?>
{
["de"] = "Hallo",
["en"] = "Hello"
}
["en"] = "Hello",
},
},
ContentCreateOptions.AsPublish,
QueryContext.Default.WithLanguages("de"));
@ -51,8 +51,8 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
{
["de"] = "Hallo",
["en"] = "Hello",
["custom"] = "Custom"
}
["custom"] = "Custom",
},
},
ContentCreateOptions.AsPublish);
@ -75,8 +75,8 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
Localized = new Dictionary<string, string?>
{
["de"] = "Hallo",
["en"] = "Hello"
}
["en"] = "Hello",
},
},
ContentCreateOptions.AsPublish);
@ -88,14 +88,14 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
content.Id,
new Dictionary<string, string>
{
["X-Flatten"] = "1"
["X-Flatten"] = "1",
});
var (etag3, _) = await GetEtagAsync(
content.Id,
new Dictionary<string, string>
{
["X-Languages"] = "en"
["X-Languages"] = "en",
});
var (etag4, _) = await GetEtagAsync(
@ -103,7 +103,7 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
new Dictionary<string, string>
{
["X-Languages"] = "en",
["X-Flatten"] = "1"
["X-Flatten"] = "1",
});
static void AssertValue(string? value, string? not = null)

6
tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs

@ -35,8 +35,8 @@ public sealed class ContentQueryFixture : TestSchemaFixtureBase
{
nested1 = new
{
nested2 = index
}
nested2 = index,
},
}),
Geo = GeoJson.Point(
index + 100,
@ -45,7 +45,7 @@ public sealed class ContentQueryFixture : TestSchemaFixtureBase
Searchable = $"text{index}",
Localized = new Dictionary<string, string?>
{
["en"] = index.ToString(CultureInfo.InvariantCulture)
["en"] = index.ToString(CultureInfo.InvariantCulture),
},
String = index.ToString(CultureInfo.InvariantCulture),
};

138
tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs

@ -140,8 +140,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
new
{
path = "data.number.iv"
}
path = "data.number.iv",
},
},
filter = new
{
@ -149,10 +149,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
path = "id",
op = "eq",
value = x.Id
}).ToArray()
}
}
value = x.Id,
}).ToArray(),
},
},
};
var items_1 = await _.Contents.GetAsync(q_1);
@ -210,10 +210,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
new
{
path = "data.number.iv"
}
}
}
path = "data.number.iv",
},
},
},
};
var items = await _.Contents.GetAsync(q);
@ -238,8 +238,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
JsonQuery = new
{
random = 5
}
random = 5,
},
};
var items = await _.Contents.GetAsync(q);
@ -268,11 +268,11 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
new
{
path = "data.number.iv"
}
path = "data.number.iv",
},
},
skip = 5
}
skip = 5,
},
};
var items = await _.Contents.GetAsync(q);
@ -302,11 +302,11 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
new
{
path = "data.number.iv"
}
path = "data.number.iv",
},
},
top = 5
}
top = 5,
},
};
var items = await _.Contents.GetAsync(q);
@ -335,8 +335,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
new
{
path = "data.number.iv"
}
path = "data.number.iv",
},
},
filter = new
{
@ -346,17 +346,17 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
path = "data.number.iv",
op = "gt",
value = 3
value = 3,
},
new
{
path = "data.number.iv",
op = "lt",
value = 7
}
}
}
}
value = 7,
},
},
},
},
};
var items = await _.Contents.GetAsync(q);
@ -375,8 +375,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
new
{
path = "data.json.iv.nested1.nested2"
}
path = "data.json.iv.nested1.nested2",
},
},
filter = new
{
@ -386,17 +386,17 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
path = "data.json.iv.nested1.nested2",
op = "gt",
value = 3
value = 3,
},
new
{
path = "data.json.iv.nested1.nested2",
op = "lt",
value = 7
}
}
}
}
value = 7,
},
},
},
},
};
var items = await _.Contents.GetAsync(q);
@ -421,8 +421,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
JsonQuery = new
{
fullText = "text2"
}
fullText = "text2",
},
};
var items = await _.Contents.PollAsync(q, x => true);
@ -455,10 +455,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
longitude = 103,
latitude = 3,
distance = 1000
}
}
}
distance = 1000,
},
},
},
};
var items = await _.Contents.PollAsync(q, x => true);
@ -488,8 +488,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
Json = new JObject
{
["search.field.with.dot"] = 42
}
["search.field.with.dot"] = 42,
},
},
ContentCreateOptions.AsPublish);
@ -507,11 +507,11 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
path = "data.json.iv.search\\.field\\.with\\.dot",
op = "eq",
value = 42
}
}
}
}
value = 42,
},
},
},
},
};
var queried = await _.Contents.GetAsync(q);
@ -542,10 +542,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
longitude = 104,
latitude = 4,
distance = 1000
}
}
}
distance = 1000,
},
},
},
};
var items = await _.Contents.PollAsync(q, x => true);
@ -572,7 +572,7 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
@ -604,10 +604,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{
number = new
{
iv = 998
}
}
}
iv = 998,
},
},
},
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
@ -635,8 +635,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}",
variables = new
{
filter = @"data/number/iv gt 3 and data/number/iv lt 7"
}
filter = @"data/number/iv gt 3 and data/number/iv lt 7",
},
};
var query2 = new
@ -654,8 +654,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}",
variables = new
{
filter = @"data/number/iv gt 4 and data/number/iv lt 7"
}
filter = @"data/number/iv gt 4 and data/number/iv lt 7",
},
};
var results = await _.Client.SharedDynamicContents.GraphQlAsync<QueryResult>([query1, query2]);
@ -685,8 +685,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}",
variables = new
{
filter = @"data/number/iv gt 3 and data/number/iv lt 7"
}
filter = @"data/number/iv gt 3 and data/number/iv lt 7",
},
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<QueryResult>(query);
@ -713,8 +713,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}",
variables = new
{
filter = @"data/number/iv gt 3 and data/number/iv lt 7"
}
filter = @"data/number/iv gt 3 and data/number/iv lt 7",
},
};
var result = await _.Client.SharedDynamicContents.GraphQlGetAsync<QueryResult>(query);
@ -738,7 +738,7 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
@ -765,8 +765,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}",
variables = new
{
search = @"The answer is 42"
}
search = @"The answer is 42",
},
};
await _.Client.SharedDynamicContents.GraphQlAsync<QueryResult>(query);

42
tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs

@ -24,7 +24,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = null
References = null,
});
@ -32,7 +32,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentB_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = [contentA_1.Id]
References = [contentA_1.Id],
},
ContentCreateOptions.AsPublish);
@ -48,7 +48,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
contentA_1.Id,
new ChangeStatus
{
Status = "Published"
Status = "Published",
});
@ -65,7 +65,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = null
References = null,
},
ContentCreateOptions.AsPublish);
@ -74,7 +74,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = [contentA_1.Id]
References = [contentA_1.Id],
},
ContentCreateOptions.AsPublish);
@ -99,7 +99,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = null
References = null,
},
ContentCreateOptions.AsPublish);
@ -108,7 +108,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = [contentA_1.Id]
References = [contentA_1.Id],
},
ContentCreateOptions.AsPublish);
@ -122,7 +122,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{
Status = "Draft",
// Ensure that the flag is true.
CheckReferrers = true
CheckReferrers = true,
});
});
@ -134,7 +134,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{
Status = "Draft",
// It is the default anyway, just to make it more explicit.
CheckReferrers = false
CheckReferrers = false,
});
}
@ -145,7 +145,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = null
References = null,
},
ContentCreateOptions.AsPublish);
@ -154,7 +154,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = [contentA_1.Id]
References = [contentA_1.Id],
},
ContentCreateOptions.AsPublish);
@ -169,10 +169,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{
Id = contentA_1.Id,
Type = BulkUpdateType.Delete,
Status = "Draft"
Status = "Draft",
},
],
CheckReferrers = true
CheckReferrers = true,
});
Assert.NotNull(result1[0].Error);
@ -188,10 +188,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{
Id = contentA_1.Id,
Type = BulkUpdateType.Delete,
Status = "Draft"
Status = "Draft",
},
],
CheckReferrers = false
CheckReferrers = false,
});
Assert.Null(result2[0].Error);
@ -204,7 +204,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = null
References = null,
},
ContentCreateOptions.AsPublish);
@ -213,7 +213,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync(
new TestEntityWithReferencesData
{
References = [contentA_1.Id]
References = [contentA_1.Id],
},
ContentCreateOptions.AsPublish);
@ -228,10 +228,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{
Id = contentA_1.Id,
Type = BulkUpdateType.ChangeStatus,
Status = "Draft"
Status = "Draft",
},
],
CheckReferrers = true
CheckReferrers = true,
});
Assert.NotNull(result1[0].Error);
@ -247,10 +247,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{
Id = contentA_1.Id,
Type = BulkUpdateType.ChangeStatus,
Status = "Draft"
Status = "Draft",
},
],
CheckReferrers = false
CheckReferrers = false,
});
Assert.Null(result2[0].Error);

28
tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs

@ -27,7 +27,7 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
{
Create = @$"
ctx.data.{TestEntityData.NumberField}.iv *= 2;
replace()"
replace()",
};
// STEP 1: Create a schema.
@ -40,7 +40,7 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
var content = await contents.CreateAsync(
new TestEntityData
{
Number = 13
Number = 13,
});
Assert.Equal(26, content.Data.Number);
@ -66,7 +66,7 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
var content = await contents.CreateAsync(
new TestEntityData
{
Number = 13
Number = 13,
},
ContentCreateOptions.AsPublish);
@ -95,7 +95,7 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
var content = await contents.CreateAsync(
new TestEntityData
{
Number = 99
Number = 99,
},
ContentCreateOptions.AsPublish);
@ -110,7 +110,7 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
{
Create = @$"
ctx.data.{TestEntityData.NumberField}.iv = incrementCounter('${schemaName}');
replace()"
replace()",
};
await TestEntity.CreateSchemaAsync(_.Client.Schemas, schemaName, scripts);
@ -133,12 +133,12 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
{
number = new
{
iv = 99
}
}
iv = 99,
},
},
},
],
Publish = true
Publish = true,
});
Assert.Single(results);
@ -159,7 +159,7 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
{
Create = @$"
ctx.data.{TestEntityData.NumberField}.iv = incrementCounter('${schemaName}');
replace()"
replace()",
};
await TestEntity.CreateSchemaAsync(_.Client.Schemas, schemaName, scripts);
@ -182,12 +182,12 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture<Cr
{
number = new
{
iv = 99
}
}
iv = 99,
},
},
},
],
Publish = true
Publish = true,
});
Assert.Single(results);

210
tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -28,7 +28,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
});
@ -37,7 +37,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new ChangeStatus
{
Status = "Published"
Status = "Published",
});
@ -52,7 +52,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
ContentCreateOptions.AsPublish);
@ -62,7 +62,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new ChangeStatus
{
Status = "Archived"
Status = "Archived",
});
@ -80,7 +80,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
});
@ -89,14 +89,14 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new ChangeStatus
{
Status = "Published"
Status = "Published",
});
await _.Contents.ChangeStatusAsync(
content.Id,
new ChangeStatus
{
Status = "Draft"
Status = "Draft",
});
@ -116,7 +116,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
String = text
String = text,
},
ContentCreateOptions.AsPublish);
@ -138,8 +138,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
Localized = new Dictionary<string, string?>
{
["en"] = null
}
["en"] = null,
},
},
ContentCreateOptions.AsPublish);
@ -161,8 +161,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
Json = new JObject
{
["field.with.dot"] = 42
}
["field.with.dot"] = 42,
},
},
ContentCreateOptions.AsPublish);
@ -182,7 +182,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Localized = []
Localized = [],
},
ContentCreateOptions.AsPublish);
@ -200,7 +200,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
});
@ -220,7 +220,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
ContentCreateOptions.AsPublish);
@ -238,7 +238,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
ContentCreateOptions.AsPublish);
@ -252,7 +252,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new TestEntityData
{
Number = 2
Number = 2,
});
var updated_1 = await _.Contents.GetAsync(content.Id);
@ -271,7 +271,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new ChangeStatus
{
Status = "Published"
Status = "Published",
});
var updated_2 = await _.Contents.GetAsync(content.Id);
@ -288,7 +288,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
new ContentCreateOptions { Id = id, Publish = true });
@ -304,7 +304,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
new ContentCreateOptions { Id = id, Publish = true });
@ -317,7 +317,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
return _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
new ContentCreateOptions { Id = id, Publish = true });
});
@ -335,7 +335,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
id,
new TestEntityData
{
Number = 1
Number = 1,
},
ContentUpsertOptions.AsPublish);
@ -347,7 +347,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
id,
new TestEntityData
{
Number = 2
Number = 2,
});
Assert.Equal(2, content.Data.Number);
@ -358,7 +358,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
id,
new TestEntityData
{
Number = 3
Number = 3,
});
Assert.Equal(3, content.Data.Number);
@ -399,7 +399,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
String = "2"
String = "2",
},
ContentCreateOptions.AsPublish);
@ -409,7 +409,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content,
new TestEntityData
{
Number = 200
Number = 200,
},
strategy);
@ -428,7 +428,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 100
Number = 100,
},
ContentCreateOptions.AsPublish);
@ -442,9 +442,9 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
["iv"] = new JObject
{
["$update"] = "$data.number.iv + 42"
}
}
["$update"] = "$data.number.iv + 42",
},
},
});
var updated = await _.Contents.GetAsync(content.Id);
@ -464,7 +464,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
String = "Hello",
// Not relevant for the test, but required in the schema.
Number = 100
Number = 100,
},
ContentCreateOptions.AsPublish);
@ -475,8 +475,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
[TestEntityData.StringField] = new JObject
{
["$unset"] = true
}
["$unset"] = true,
},
});
var updated = await _.Contents.GetAsync(content.Id);
@ -493,7 +493,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
String = "Hello",
// Not relevant for the test, but required in the schema.
Number = 100
Number = 100,
},
ContentCreateOptions.AsPublish);
@ -506,9 +506,9 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
["iv"] = new JObject
{
["$unset"] = true
}
}
["$unset"] = true,
},
},
});
var updated = await _.Contents.GetAsync(content.Id);
@ -529,7 +529,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
String = "initial"
String = "initial",
},
ContentCreateOptions.AsPublish);
@ -539,7 +539,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content,
new TestEntityData
{
String = null
String = null,
},
strategy);
@ -561,7 +561,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
String = "initial"
String = "initial",
},
ContentCreateOptions.AsPublish);
@ -571,7 +571,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content,
new TestEntityData
{
Number = 200
Number = 200,
},
strategy);
@ -590,7 +590,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 100
Number = 100,
},
ContentCreateOptions.AsPublish);
@ -603,9 +603,9 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
["iv"] = new JObject
{
["$update"] = "$data.number.iv + 42"
}
}
["$update"] = "$data.number.iv + 42",
},
},
});
var updated = await _.Contents.GetAsync(content.Id);
@ -629,7 +629,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Id = "id1"
Id = "id1",
},
ContentCreateOptions.AsPublish);
@ -639,7 +639,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content,
new TestEntityData
{
Id = "id2"
Id = "id2",
},
strategy);
@ -661,7 +661,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
String = "initial"
String = "initial",
},
ContentCreateOptions.AsPublish);
@ -671,7 +671,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content,
new
{
@string = new { iv = (string?)null }
@string = new { iv = (string?)null },
},
strategy);
@ -691,7 +691,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 2
Number = 2,
},
ContentCreateOptions.AsPublish);
@ -727,7 +727,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
new ContentCreateOptions { Id = id, Publish = true });
@ -755,7 +755,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content_1 = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 2
Number = 2,
},
ContentCreateOptions.AsPublish);
@ -768,7 +768,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content_2 = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 2
Number = 2,
},
new ContentCreateOptions { Id = content_1.Id, Publish = true });
@ -794,7 +794,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content_1 = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 2
Number = 2,
},
ContentCreateOptions.AsPublish);
@ -808,7 +808,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content_1.Id,
new TestEntityData
{
Number = 2
Number = 2,
},
ContentUpsertOptions.AsPublish);
@ -830,7 +830,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content_1 = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
options);
@ -843,7 +843,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content_2 = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 2
Number = 2,
},
options);
@ -870,9 +870,9 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
new UpsertSchemaFieldDto
{
Name = "my-field",
Properties = new StringFieldPropertiesDto()
Properties = new StringFieldPropertiesDto(),
},
]
],
};
await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -893,8 +893,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
["my-field"] = new JObject
{
["iv"] = "singleton"
}
["iv"] = "singleton",
},
});
Assert.Equal("singleton", content_2.Data["my-field"]["iv"]);
@ -909,7 +909,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 1
Number = 1,
},
ContentCreateOptions.AsPublish);
@ -919,7 +919,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new TestEntityData
{
Number = 2
Number = 2,
});
@ -950,7 +950,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 2
Number = 2,
},
ContentCreateOptions.AsPublish);
@ -964,7 +964,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new TestEntityData
{
Number = i
Number = i,
},
ct: ct);
}
@ -980,7 +980,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new TestEntityData
{
Number = 2
Number = 2,
});
var updated = await _.Contents.GetAsync(content.Id);
@ -995,7 +995,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Number = 2
Number = 2,
},
ContentCreateOptions.AsPublish);
@ -1009,7 +1009,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new TestEntityData
{
Number = i
Number = i,
},
ct: ct);
}
@ -1025,7 +1025,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
content.Id,
new TestEntityData
{
Number = 2
Number = 2,
});
var updated = await _.Contents.GetAsync(content.Id);
@ -1050,7 +1050,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var schemaRequest = new CreateSchemaDto
{
Name = schemaName,
IsPublished = true
IsPublished = true,
};
await _.Client.Schemas.PostSchemaAsync(schemaRequest);
@ -1071,8 +1071,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
Properties = new StringFieldPropertiesDto
{
DefaultValue = "Hello Squidex",
IsRequired = false
}
IsRequired = false,
},
};
await _.Client.Schemas.PostFieldAsync(schemaName, fieldRequest);
@ -1106,7 +1106,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var schemaRequest = new CreateSchemaDto
{
Name = schemaName,
IsPublished = true
IsPublished = true,
};
await _.Client.Schemas.PostSchemaAsync(schemaRequest);
@ -1128,8 +1128,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
Properties = new StringFieldPropertiesDto
{
DefaultValue = "Hello Squidex",
IsRequired = false
}
IsRequired = false,
},
};
var fieldRequest2 = new AddFieldDto
@ -1138,8 +1138,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
Properties = new StringFieldPropertiesDto
{
DefaultValue = "Hello Required",
IsRequired = true
}
IsRequired = true,
},
};
await _.Client.Schemas.PostFieldAsync(schemaName, fieldRequest1);
@ -1175,7 +1175,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var schemaRequest = new CreateSchemaDto
{
Name = schemaName,
IsPublished = true
IsPublished = true,
};
await _.Client.Schemas.PostSchemaAsync(schemaRequest);
@ -1197,8 +1197,8 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
Properties = new StringFieldPropertiesDto
{
DefaultValue = "Hello Required",
IsRequired = true
}
IsRequired = true,
},
};
await _.Client.Schemas.PostFieldAsync(schemaName, fieldRequest);
@ -1235,13 +1235,13 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
[TestEntityData.StringField] = new
{
iv = $"{prefix}_1"
iv = $"{prefix}_1",
},
[TestEntityData.NumberField] = new
{
iv = 1
}
}
iv = 1,
},
},
},
new BulkUpdateJob
{
@ -1249,16 +1249,16 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
[TestEntityData.StringField] = new
{
iv = $"{prefix}_2"
iv = $"{prefix}_2",
},
[TestEntityData.NumberField] = new
{
iv = 2
}
}
iv = 2,
},
},
},
],
Publish = true
Publish = true,
});
result_0 = result_0.OrderBy(x => x.JobIndex).ToList();
@ -1278,17 +1278,17 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
path = $"data.{TestEntityData.StringField}.iv",
op = "eq",
value = $"{prefix}_1"
}
value = $"{prefix}_1",
},
},
Data = new Dictionary<string, object>
{
[TestEntityData.StringField] = new
{
iv = $"{prefix}_1_x"
}
iv = $"{prefix}_1_x",
},
},
Type = BulkUpdateType.Patch
Type = BulkUpdateType.Patch,
},
new BulkUpdateJob
{
@ -1298,19 +1298,19 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
{
path = $"data.{TestEntityData.StringField}.iv",
op = "eq",
value = $"{prefix}_2"
}
value = $"{prefix}_2",
},
},
Data = new Dictionary<string, object>
{
[TestEntityData.StringField] = new
{
iv = $"{prefix}_2_y"
}
iv = $"{prefix}_2_y",
},
},
Type = BulkUpdateType.Patch
Type = BulkUpdateType.Patch,
},
]
],
});
result_1.OrderBy(x => x.JobIndex).Should().BeEquivalentTo(
@ -1318,12 +1318,12 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
new BulkResult
{
ContentId = result_0[0].ContentId,
JobIndex = 0
JobIndex = 0,
},
new BulkResult
{
ContentId = result_0[1].ContentId,
JobIndex = 1
JobIndex = 1,
},
]);
@ -1332,7 +1332,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var contents = await _.Contents.GetAsync(
new ContentQuery
{
Ids = result_0.Select(x => x.ContentId).ToHashSet()
Ids = result_0.Select(x => x.ContentId).ToHashSet(),
});
var content0 = contents.Items.Find(x => x.Id == result_0[0].ContentId);
@ -1349,7 +1349,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var content = await _.Contents.CreateAsync(
new TestEntityData
{
Immutable = "v1"
Immutable = "v1",
},
ContentCreateOptions.AsPublish);
@ -1366,7 +1366,7 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture<ContentF
var ex = await Assert.ThrowsAsync<SquidexException<ErrorDto>>(() => _.Contents.UpdateAsync(content));
Assert.Equal(400, ex.StatusCode);
Assert.Contains("Validation error: immutable.iv: Field cannot be changed", ex.ToString());
Assert.Contains("Validation error: immutable.iv: Field cannot be changed", ex.ToString(), StringComparison.Ordinal);
}
[Fact]

60
tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs

@ -71,10 +71,10 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
Properties = new StringFieldPropertiesDto(),
},
],
Type = SchemaType.Component
Type = SchemaType.Component,
};
var locationsId = await CreateSchemaAsync(createLocationRequest);
@ -89,7 +89,7 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
Properties = new StringFieldPropertiesDto(),
},
new UpsertSchemaFieldDto
{
@ -98,9 +98,9 @@ public sealed class GraphQLFixture : ContentFixture
{
SchemaIds =
[
locationsId
]
}
locationsId,
],
},
},
new UpsertSchemaFieldDto
{
@ -109,12 +109,12 @@ public sealed class GraphQLFixture : ContentFixture
{
SchemaIds =
[
locationsId
]
}
locationsId,
],
},
},
],
IsPublished = true
IsPublished = true,
};
var citiesId = await CreateSchemaAsync(createCitiesRequest);
@ -129,18 +129,18 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
Properties = new StringFieldPropertiesDto(),
},
new UpsertSchemaFieldDto
{
Name = "cities",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = [citiesId]
}
SchemaIds = [citiesId],
},
},
],
IsPublished = true
IsPublished = true,
};
var statesId = await CreateSchemaAsync(createStatesRequest);
@ -155,18 +155,18 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
Properties = new StringFieldPropertiesDto(),
},
new UpsertSchemaFieldDto
{
Name = "states",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = [statesId]
}
SchemaIds = [statesId],
},
},
],
IsPublished = true
IsPublished = true,
};
await CreateSchemaAsync(createCountriesRequest);
@ -187,14 +187,14 @@ public sealed class GraphQLFixture : ContentFixture
{
name = new
{
iv = name
iv = name,
},
topLocation = new
{
iv = new
{
name = $"{name} Top Location"
}
name = $"{name} Top Location",
},
},
locations = new
{
@ -207,9 +207,9 @@ public sealed class GraphQLFixture : ContentFixture
new
{
name = $"{name} Location 2",
}
}
}
},
},
},
};
var city = await Cities.CreateAsync(cityData, ContentCreateOptions.AsPublish);
@ -223,12 +223,12 @@ public sealed class GraphQLFixture : ContentFixture
{
name = new
{
iv = name
iv = name,
},
cities = new
{
iv = new[] { cityId }
}
iv = new[] { cityId },
},
};
var state = await States.CreateAsync(stateData, ContentCreateOptions.AsPublish);
@ -251,12 +251,12 @@ public sealed class GraphQLFixture : ContentFixture
{
name = new
{
iv = "Germany"
iv = "Germany",
},
states = new
{
iv = new[] { saxonyState, bavariaState }
}
iv = new[] { saxonyState, bavariaState },
},
};
await Countries.CreateAsync(countryData, ContentCreateOptions.AsPublish);

6
tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs

@ -55,7 +55,7 @@ public class GraphQLSubscriptionTests(ContentFixture fixture) : IClassFixture<Co
contentChanges {
id
}
}"
}",
};
var contentId = Guid.NewGuid().ToString();
@ -95,7 +95,7 @@ public class GraphQLSubscriptionTests(ContentFixture fixture) : IClassFixture<Co
assetChanges {
id
}
}"
}",
};
var assetId = Guid.NewGuid().ToString();
@ -128,7 +128,7 @@ public class GraphQLSubscriptionTests(ContentFixture fixture) : IClassFixture<Co
var options = new GraphQLHttpClientOptions
{
EndPoint = new Uri(_.Client.GenerateUrl($"/api/content/{_.AppName}/graphql?access_token={accessToken}")!)
EndPoint = new Uri(_.Client.GenerateUrl($"/api/content/{_.AppName}/graphql?access_token={accessToken}")!),
};
var client = new GraphQLHttpClient(options, new NewtonsoftJsonSerializer());

60
tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

@ -73,7 +73,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
variables = new
{
id = asset.Id,
}
},
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -100,9 +100,9 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
value = 1,
obj = new
{
value = 2
}
})
value = 2,
},
}),
},
ContentCreateOptions.AsPublish);
@ -121,7 +121,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
variables = new
{
id = content_0.Id,
}
},
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -148,15 +148,15 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cities = result?["cities"]?.ToObject<List<City>>();
cities.Should().BeEquivalentTo(new List<City>
{
cities.Should().BeEquivalentTo(
[
new City
{
Data = new CityData
@ -164,20 +164,20 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
Name = "Leipzig",
TopLocation = new LocationData
{
Name = "Leipzig Top Location"
Name = "Leipzig Top Location",
},
Locations =
[
new LocationData
{
Name = "Leipzig Location 1"
Name = "Leipzig Location 1",
},
new LocationData
{
Name = "Leipzig Location 2"
Name = "Leipzig Location 2",
},
],
}
},
},
new City
{
@ -186,22 +186,22 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
Name = "Munich",
TopLocation = new LocationData
{
Name = "Munich Top Location"
Name = "Munich Top Location",
},
Locations =
[
new LocationData
{
Name = "Munich Location 1"
Name = "Munich Location 1",
},
new LocationData
{
Name = "Munich Location 2"
Name = "Munich Location 2",
},
],
}
}
});
},
},
]);
}
[Fact]
@ -226,7 +226,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -262,7 +262,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -299,7 +299,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -328,7 +328,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -356,7 +356,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}
}
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -380,7 +380,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
cities: queryCitiesContents {
data__dynamic
}
}"
}",
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -403,7 +403,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
queryCitiesContents {
id
}
}"
}",
};
var url = _.Client.GenerateUrl($"api/content/{_.AppName}/graphql/batch");
@ -445,8 +445,8 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}",
variables = new
{
ids
}
ids,
},
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -485,7 +485,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
{
id1 = allCities.Items[0].Id,
id2 = allCities.Items[1].Id,
}
},
};
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -507,7 +507,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
queryCitiesContents {
id
}
}"
}",
};
// Create the request manually to check the headers.
@ -526,7 +526,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
"X-NoResolveLanguages",
"X-ResolveFlow",
"X-ResolveUrls",
"X-Unpublished"
"X-Unpublished",
}, httpResponse.Headers.Vary.Order().ToArray());
}
}

8
tools/TestSuite/TestSuite.ApiTests/RuleEventsTests.cs

@ -105,10 +105,10 @@ public class RuleEventsTests(ClientFixture fixture) : IClassFixture<ClientFixtur
Method = WebhookMethod.POST,
Payload = null,
PayloadType = null,
Url = new Uri("http://squidex.io")
}
}
}
Url = "http://squidex.io",
},
},
},
},
Trigger = new ManualRuleTriggerDto(),
};

148
tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs

@ -50,17 +50,17 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Step = new WebhookFlowStepDto
{
Url = new Uri(url),
Url = url,
// Also test the secret in this case.
SharedSecret = secret
}
}
}
SharedSecret = secret,
},
},
},
},
Trigger = new ContentChangedRuleTriggerDto
{
HandleAll = true
}
HandleAll = true,
},
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -103,7 +103,7 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
var referencedContent = await referencedContents.CreateAsync(
new TestEntityData
{
String = contentString
String = contentString,
});
var parentSchema = await TestEntityWithReferences.CreateSchemaAsync(app.Schemas, schemaNameRef);
@ -116,7 +116,7 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
References =
[
referencedContent.Id
referencedContent.Id,
],
});
@ -142,12 +142,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
complete(payload);
}});
)",
Url = new Uri(url),
Url = url,
// Also test the secret in this case.
SharedSecret = secret
}
}
}
SharedSecret = secret,
},
},
},
},
Trigger = new ContentChangedRuleTriggerDto
{
@ -155,17 +155,17 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
[
new SchemaCondition
{
SchemaId = parentSchema.Id
SchemaId = parentSchema.Id,
},
],
ReferencedSchemas =
[
new SchemaCondition
{
SchemaId = referencedSchema.Id
SchemaId = referencedSchema.Id,
},
]
}
],
},
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -179,7 +179,7 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
referencedContent.Id,
new TestEntityData
{
String = updatedString
String = updatedString,
});
@ -222,15 +222,15 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Script = $@"
postJSON('{url}', {{ schemaName: event.schemaId.Name }}, function () {{}})
"
}
}
}
",
},
},
},
},
Trigger = new ContentChangedRuleTriggerDto
{
HandleAll = true
}
HandleAll = true,
},
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -276,14 +276,14 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Step = new WebhookFlowStepDto
{
Url = new Uri(url),
Url = url,
// Also test the secret in this case.
SharedSecret = secret
}
}
}
SharedSecret = secret,
},
},
},
},
Trigger = new AssetChangedRuleTriggerDto()
Trigger = new AssetChangedRuleTriggerDto(),
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -335,12 +335,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
updateAsset(event, metadata);
});
"
}
}
}
",
},
},
},
},
Trigger = new AssetChangedRuleTriggerDto()
Trigger = new AssetChangedRuleTriggerDto(),
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -378,14 +378,14 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Step = new WebhookFlowStepDto
{
Url = new Uri(url),
Url = url,
// Also test the secret in this case.
SharedSecret = secret
}
}
}
SharedSecret = secret,
},
},
},
},
Trigger = new SchemaChangedRuleTriggerDto()
Trigger = new SchemaChangedRuleTriggerDto(),
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -431,14 +431,14 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Step = new WebhookFlowStepDto
{
Url = new Uri(url),
Url = url,
// Also test the secret in this case.
SharedSecret = secret
}
}
}
SharedSecret = secret,
},
},
},
},
Trigger = new ManualRuleTriggerDto()
Trigger = new ManualRuleTriggerDto(),
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -486,17 +486,17 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Step = new WebhookFlowStepDto
{
Url = new Uri(url),
Url = url,
// Also test the secret in this case.
SharedSecret = secret
}
}
}
SharedSecret = secret,
},
},
},
},
Trigger = new ContentChangedRuleTriggerDto
{
HandleAll = true
}
HandleAll = true,
},
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -544,12 +544,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
console.log('Hello Log');
console.warn('Hello warn');
"
}
}
}
",
},
},
},
},
Trigger = new ManualRuleTriggerDto()
Trigger = new ManualRuleTriggerDto(),
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -596,8 +596,8 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
Condition = "event.value.testValue == 2",
NextStepId = stepId3,
},
]
}
],
},
},
[stepId2.ToString()] = new FlowStepDefinitionDto
{
@ -606,8 +606,8 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
Script = @"
console.info('Hello from Branch1');
"
}
",
},
},
[stepId3.ToString()] = new FlowStepDefinitionDto
{
@ -616,12 +616,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
Script = @"
console.info('Hello from Branch2');
"
}
}
}
",
},
},
},
},
Trigger = new ManualRuleTriggerDto()
Trigger = new ManualRuleTriggerDto(),
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -632,8 +632,8 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Value = new Dictionary<string, int>
{
["testValue"] = 1
}
["testValue"] = 1,
},
});
var @event1 = await app.Rules.PollEventAsync(rule.Id, x => x.FlowState.Status == FlowExecutionStatus.Completed);
@ -651,8 +651,8 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{
Value = new Dictionary<string, int>
{
["testValue"] = 2
}
["testValue"] = 2,
},
});
var @event2 = await app.Rules.PollEventAsync(rule.Id, x => x.FlowState.Status == FlowExecutionStatus.Completed && x.Id != event1!.Id);
@ -675,7 +675,7 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
await contents.CreateAsync(
new TestEntityData
{
String = contentString
String = contentString,
});
}

44
tools/TestSuite/TestSuite.ApiTests/RuleTests.cs

@ -43,15 +43,15 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
Method = WebhookMethod.POST,
Payload = null,
PayloadType = null,
Url = new Uri("http://squidex.io")
}
}
}
Url = "http://squidex.io",
},
},
},
},
Trigger = new ContentChangedRuleTriggerDto
{
HandleAll = true
}
HandleAll = true,
},
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -76,12 +76,12 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
Method = WebhookMethod.POST,
Payload = null,
PayloadType = null,
Url = new Uri("http://squidex.io")
Url = new Uri("http://squidex.io"),
},
Trigger = new ContentChangedRuleTriggerDto
{
HandleAll = true
}
HandleAll = true,
},
};
var rule = await app.Rules.PostRuleAsync(createRule);
@ -112,15 +112,15 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
Method = WebhookMethod.POST,
Payload = null,
PayloadType = null,
Url = new Uri("http://squidex.io")
}
}
}
Url = "http://squidex.io",
},
},
},
},
Trigger = new ContentChangedRuleTriggerDto
{
HandleAll = true
}
HandleAll = true,
},
};
var rule_0 = await app.Rules.PostRuleAsync(createRequest);
@ -129,7 +129,7 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
// STEP 2: Update rule.
var updateRequest = new UpdateRuleDto
{
Name = ruleName
Name = ruleName,
};
var rule_1 = await app.Rules.PutRuleAsync(rule_0.Id, updateRequest);
@ -160,15 +160,15 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
Method = WebhookMethod.POST,
Payload = null,
PayloadType = null,
Url = new Uri("http://squidex.io")
}
}
}
Url = "http://squidex.io",
},
},
},
},
Trigger = new ContentChangedRuleTriggerDto
{
HandleAll = true
}
HandleAll = true,
},
};
var rule = await app.Rules.PostRuleAsync(createRequest);

2
tools/TestSuite/TestSuite.ApiTests/RuleValidationTests.cs

@ -64,7 +64,7 @@ public class RuleValidationTests(CreatedAppFixture fixture) : IClassFixture<Crea
[Fact]
public async Task Should_validate_valid_step()
{
var trigger = new WebhookFlowStepDto { Url = new Uri("https://squidex.io") };
var trigger = new WebhookFlowStepDto { Url = "https://squidex.io" };
await _.Client.Rules.ValidateStepAsync(trigger);
}

22
tools/TestSuite/TestSuite.ApiTests/SchemaTests.cs

@ -26,7 +26,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// STEP 1: Create schema.
var createRequest = new CreateSchemaDto
{
Name = schemaName
Name = schemaName,
};
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -48,7 +48,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// STEP 1: Create schema.
var createRequest = new CreateSchemaDto
{
Name = schemaName
Name = schemaName,
};
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -73,7 +73,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// Use the new property to create a singleton.
Type = SchemaType.Singleton,
// Must be pusblished to query content.
IsPublished = true
IsPublished = true,
};
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -108,7 +108,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// Use the old property to create a singleton.
IsSingleton = true,
// Must be pusblished to query content.
IsPublished = true
IsPublished = true,
};
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -148,8 +148,8 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
Partitioning = "invariant",
Properties = new ReferencesFieldPropertiesDto
{
Editor = ReferencesFieldEditor.Checkboxes
}
Editor = ReferencesFieldEditor.Checkboxes,
},
},
new UpsertSchemaFieldDto
{
@ -158,10 +158,10 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
Properties = new TagsFieldPropertiesDto
{
Editor = TagsFieldEditor.Checkboxes,
AllowedValues = ["value1"]
}
AllowedValues = ["value1"],
},
},
]
],
};
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -176,7 +176,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// STEP 1: Create schema.
var createRequest = new CreateSchemaDto
{
Name = schemaName
Name = schemaName,
};
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -200,7 +200,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// STEP 1: Create schema.
var createRequest = new CreateSchemaDto
{
Name = schemaName
Name = schemaName,
};
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);

4
tools/TestSuite/TestSuite.ApiTests/SearchTests.cs

@ -38,7 +38,7 @@ public class SearchTests(ContentFixture fixture) : IClassFixture<ContentFixture>
var createRequest = new CreateSchemaDto
{
Name = schemaName
Name = schemaName,
};
await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -59,7 +59,7 @@ public class SearchTests(ContentFixture fixture) : IClassFixture<ContentFixture>
var createRequest = new TestEntityData
{
String = contentString
String = contentString,
};
await _.Contents.CreateAsync(createRequest, ContentCreateOptions.AsPublish);

4
tools/TestSuite/TestSuite.ApiTests/TeamContributorTests.cs

@ -27,7 +27,7 @@ public class TeamContributorTests(ClientFixture fixture) : IClassFixture<ClientF
// STEP 1: Do not invite contributors when flag is false.
var createRequest = new AssignContributorDto
{
ContributorId = "test@squidex.io"
ContributorId = "test@squidex.io",
};
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() =>
@ -111,7 +111,7 @@ public class TeamContributorTests(ClientFixture fixture) : IClassFixture<ClientF
// Invite must be true, otherwise new users are not created.
Invite = true,
// This is the only allowed role for teams.
Role = "Owner"
Role = "Owner",
};
var contributors = await _.Client.Teams.PostContributorAsync(teamId, createInviteRequest);

14
tools/TestSuite/TestSuite.ApiTests/TeamTests.cs

@ -24,7 +24,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 1: Update app.
var updateRequest = new UpdateTeamDto
{
Name = Guid.NewGuid().ToString()
Name = Guid.NewGuid().ToString(),
};
var app_1 = await _.Client.Teams.PutTeamAsync(_.TeamId, updateRequest);
@ -42,7 +42,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 1: Assign app to team.
var transferRequest = new TransferToTeamDto
{
TeamId = _.TeamId
TeamId = _.TeamId,
};
var app_1 = await client.Apps.PutAppTeamAsync(transferRequest);
@ -60,7 +60,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 1: Assign app to team.
var transferRequest = new TransferToTeamDto
{
TeamId = _.TeamId
TeamId = _.TeamId,
};
var app_1 = await client.Apps.PutAppTeamAsync(transferRequest);
@ -71,7 +71,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 2: Remove app from team.
var untransferRequest = new TransferToTeamDto
{
TeamId = null
TeamId = null,
};
var app_2 = await client.Apps.PutAppTeamAsync(untransferRequest);
@ -94,7 +94,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 1: Assign scheme.
var request = new AuthSchemeValueDto
{
Scheme = scheme
Scheme = scheme,
};
var scheme_0 = await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request);
@ -123,7 +123,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 0: Assign scheme.
var request1 = new AuthSchemeValueDto
{
Scheme = scheme
Scheme = scheme,
};
await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request1);
@ -132,7 +132,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 1: Unassign scheme.
var request2 = new AuthSchemeValueDto
{
Scheme = null
Scheme = null,
};
var scheme_0 = await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request2);

4
tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -16,8 +16,8 @@
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NSwag.Core" Version="14.4.0" />
<PackageReference Include="Squidex.Assets" Version="7.24.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.24.0" />
<PackageReference Include="Squidex.Assets" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Verify.Xunit" Version="30.4.0" />
<PackageReference Include="xunit" Version="2.9.3" />

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

Loading…
Cancel
Save