Browse Source

Generate schema with ai (#1238)

* Fix prepare step.

* UI fixes.

* Small fixes.

* Generate with AI.

* Update packages.
pull/1241/head
Sebastian Stehle 1 year 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); var found = await contentRepository.QueryIdsAsync(context.Root.App, context.Root.Schema, filter, SearchScope.All);
if (found.Any(x => x.Id != context.Root.ContentId)) 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; var value = JsonValue.Null;

10
backend/i18n/frontend_en.json

@ -14,7 +14,7 @@
"apps.appLoadFailed": "Failed to load app. Please reload.", "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.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.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.appsButtonCreate": "Create App",
"apps.appsButtonCreateTeam": "Create Team", "apps.appsButtonCreateTeam": "Create Team",
"apps.appsButtonFallbackTitle": "Apps and Teams", "apps.appsButtonFallbackTitle": "Apps and Teams",
@ -339,6 +339,7 @@
"common.prevPage": "Previous Page", "common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS", "common.product": "Squidex Headless CMS",
"common.project": "Project", "common.project": "Project",
"common.prompt": "Prompt",
"common.properties": "Properties", "common.properties": "Properties",
"common.queryOperators.contains": "contains", "common.queryOperators.contains": "contains",
"common.queryOperators.empty": "is empty", "common.queryOperators.empty": "is empty",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Contents Sidebar Extension", "schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.", "schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.create": "Create Schema", "schemas.create": "Create Schema",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Create new category...", "schemas.createCategory": "Create new category...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Failed to create schema. Please reload.", "schemas.createFailed": "Failed to create schema. Please reload.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "New Schema", "schemas.createSchemaTooltip": "New Schema",
"schemas.deleteConfirmText": "Do you really want to delete the schema?", "schemas.deleteConfirmText": "Do you really want to delete the schema?",
"schemas.deleteConfirmTitle": "Delete schema", "schemas.deleteConfirmTitle": "Delete schema",
@ -1007,12 +1011,14 @@
"schemas.modeMultipleDescription": "Best for multiple instances like blog posts, pages, authors, products...", "schemas.modeMultipleDescription": "Best for multiple instances like blog posts, pages, authors, products...",
"schemas.modeSingle": "Single content", "schemas.modeSingle": "Single content",
"schemas.modeSingleDescription": "Best for single instances like the home page, privacy policies, settings...", "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.empty": "No preview urls configured.",
"schemas.previewUrls.help": "Checkout the integrated help page to learn more about preview URL's.", "schemas.previewUrls.help": "Checkout the integrated help page to learn more about preview URL's.",
"schemas.previewUrls.namePlaceholder": "Web or Mobile", "schemas.previewUrls.namePlaceholder": "Web or Mobile",
"schemas.previewUrls.title": "Preview URLs", "schemas.previewUrls.title": "Preview URLs",
"schemas.previewUrls.urlPlaceholder": "URL with variables", "schemas.previewUrls.urlPlaceholder": "URL with variables",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Published", "schemas.published": "Published",
"schemas.publishFailed": "Failed to publish schema. Please reload.", "schemas.publishFailed": "Failed to publish schema. Please reload.",
"schemas.referenceFields": "Reference Fields", "schemas.referenceFields": "Reference Fields",

6
backend/i18n/frontend_fr.json

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

6
backend/i18n/frontend_it.json

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

6
backend/i18n/frontend_nl.json

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

6
backend/i18n/frontend_pt.json

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

6
backend/i18n/frontend_zh.json

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

10
backend/i18n/source/frontend_en.json

@ -14,7 +14,7 @@
"apps.appLoadFailed": "Failed to load app. Please reload.", "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.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.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.appsButtonCreate": "Create App",
"apps.appsButtonCreateTeam": "Create Team", "apps.appsButtonCreateTeam": "Create Team",
"apps.appsButtonFallbackTitle": "Apps and Teams", "apps.appsButtonFallbackTitle": "Apps and Teams",
@ -339,6 +339,7 @@
"common.prevPage": "Previous Page", "common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS", "common.product": "Squidex Headless CMS",
"common.project": "Project", "common.project": "Project",
"common.prompt": "Prompt",
"common.properties": "Properties", "common.properties": "Properties",
"common.queryOperators.contains": "contains", "common.queryOperators.contains": "contains",
"common.queryOperators.empty": "is empty", "common.queryOperators.empty": "is empty",
@ -836,8 +837,11 @@
"schemas.contentsSidebarUrl": "Contents Sidebar Extension", "schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.", "schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.create": "Create Schema", "schemas.create": "Create Schema",
"schemas.createAI": "\uD83E\uDE84 With AI",
"schemas.createCategory": "Create new category...", "schemas.createCategory": "Create new category...",
"schemas.createCustom": "Custom",
"schemas.createFailed": "Failed to create schema. Please reload.", "schemas.createFailed": "Failed to create schema. Please reload.",
"schemas.createFromJson": "From JSON",
"schemas.createSchemaTooltip": "New Schema", "schemas.createSchemaTooltip": "New Schema",
"schemas.deleteConfirmText": "Do you really want to delete the schema?", "schemas.deleteConfirmText": "Do you really want to delete the schema?",
"schemas.deleteConfirmTitle": "Delete schema", "schemas.deleteConfirmTitle": "Delete schema",
@ -1007,12 +1011,14 @@
"schemas.modeMultipleDescription": "Best for multiple instances like blog posts, pages, authors, products...", "schemas.modeMultipleDescription": "Best for multiple instances like blog posts, pages, authors, products...",
"schemas.modeSingle": "Single content", "schemas.modeSingle": "Single content",
"schemas.modeSingleDescription": "Best for single instances like the home page, privacy policies, settings...", "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.empty": "No preview urls configured.",
"schemas.previewUrls.help": "Checkout the integrated help page to learn more about preview URL's.", "schemas.previewUrls.help": "Checkout the integrated help page to learn more about preview URL's.",
"schemas.previewUrls.namePlaceholder": "Web or Mobile", "schemas.previewUrls.namePlaceholder": "Web or Mobile",
"schemas.previewUrls.title": "Preview URLs", "schemas.previewUrls.title": "Preview URLs",
"schemas.previewUrls.urlPlaceholder": "URL with variables", "schemas.previewUrls.urlPlaceholder": "URL with variables",
"schemas.promptExample": "Example: \"A blog for a travel website\"",
"schemas.promptHint": "Describe your schema.",
"schemas.published": "Published", "schemas.published": "Published",
"schemas.publishFailed": "Failed to publish schema. Please reload.", "schemas.publishFailed": "Failed to publish schema. Please reload.",
"schemas.referenceFields": "Reference Fields", "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.Json.Microsoft" Version="8.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.NetTopologySuite" 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="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.25.0" /> <PackageReference Include="Squidex.AI.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.TusAdapter" Version="7.26.0" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="7.25.0" /> <PackageReference Include="Squidex.Events.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="7.25.0" /> <PackageReference Include="Squidex.Flows.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting" Version="7.25.0" /> <PackageReference Include="Squidex.Hosting" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.25.0" /> <PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.26.0" />
<PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="5.8.4" /> <PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <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="MongoDB.Driver.GridFS" Version="2.30.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.25.0" /> <PackageReference Include="Squidex.AI.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.25.0" /> <PackageReference Include="Squidex.Events.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.25.0" /> <PackageReference Include="Squidex.Flows.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting" Version="7.25.0" /> <PackageReference Include="Squidex.Hosting" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.25.0" /> <PackageReference Include="Squidex.Messaging.Mongo" Version="7.26.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.8.4" /> <PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <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="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <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="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.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="NJsonSchema" Version="11.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="7.25.0" /> <PackageReference Include="Squidex.AI" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.25.0" /> <PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <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"); 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); 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 System.Globalization;
using Newtonsoft.Json;
using Squidex.CLI.Commands.Implementation; using Squidex.CLI.Commands.Implementation;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Log; using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.Apps.Templates; 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 const int MaxActionLength = 40;
private readonly List<string> lines = []; private readonly List<string> lines = [];
@ -22,16 +23,18 @@ public sealed class StringLogger(IJsonSerializer jsonSerializer) : ILogger, ILog
public bool CanWriteToSameLine => false; public bool CanWriteToSameLine => false;
public List<string> Lines => lines;
public void Flush(ISemanticLog log, string template) public void Flush(ISemanticLog log, string template)
{ {
var mesage = string.Join('\n', lines); var mesage = string.Join('\n', Lines);
log.LogInformation(w => w log.LogInformation(w => w
.WriteProperty("message", $"CLI executed or template {template}.") .WriteProperty("message", $"CLI executed or template {template}.")
.WriteProperty("template", template) .WriteProperty("template", template)
.WriteArray("steps", a => .WriteArray("steps", a =>
{ {
foreach (var line in lines) foreach (var line in Lines)
{ {
a.WriteValue(line); a.WriteValue(line);
} }
@ -84,22 +87,22 @@ public sealed class StringLogger(IJsonSerializer jsonSerializer) : ILogger, ILog
public void WriteLine() public void WriteLine()
{ {
lines.Add(string.Empty); Lines.Add(string.Empty);
} }
public void WriteLine(string message) public void WriteLine(string message)
{ {
lines.Add(message); Lines.Add(message);
} }
public void WriteLine(string message, params object?[] args) 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) public void WriteJson(object message)
{ {
lines.Add(jsonSerializer.Serialize(message, true)); Lines.Add(JsonConvert.SerializeObject(message, Formatting.Indented));
} }
private void AddToErrors(string reason) private void AddToErrors(string reason)
@ -110,7 +113,7 @@ public sealed class StringLogger(IJsonSerializer jsonSerializer) : ILogger, ILog
private void AddToLine(string message) private void AddToLine(string message)
{ {
startedLine += message; startedLine += message;
lines.Add(startedLine); Lines.Add(startedLine);
startedLine = string.Empty; 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Microsoft.Extensions.Options;
using Squidex.CLI.Commands.Implementation; using Squidex.CLI.Commands.Implementation;
using Squidex.CLI.Commands.Implementation.FileSystem; using Squidex.CLI.Commands.Implementation.FileSystem;
using Squidex.CLI.Commands.Implementation.Sync; 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.Rules;
using Squidex.CLI.Commands.Implementation.Sync.Schemas; using Squidex.CLI.Commands.Implementation.Sync.Schemas;
using Squidex.CLI.Commands.Implementation.Sync.Workflows; 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.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -28,15 +24,11 @@ using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.Apps.Templates; namespace Squidex.Domain.Apps.Entities.Apps.Templates;
public sealed class TemplateCommandMiddleware( public sealed class TemplateCommandMiddleware(
TemplatesClient templatesClient, TemplatesClient client,
IOptions<TemplatesOptions> templateOptions, SessionFactory sessionFactory,
IUrlGenerator urlGenerator,
IJsonSerializer jsonSerializer,
ISemanticLog log) ISemanticLog log)
: ICommandMiddleware : ICommandMiddleware
{ {
private readonly TemplatesOptions templateOptions = templateOptions.Value;
public async Task HandleAsync(CommandContext context, NextDelegate next, public async Task HandleAsync(CommandContext context, NextDelegate next,
CancellationToken ct) CancellationToken ct)
{ {
@ -55,7 +47,7 @@ public sealed class TemplateCommandMiddleware(
return; return;
} }
var repository = await templatesClient.GetRepositoryUrl(template); var repository = await client.GetRepositoryUrl(template);
if (string.IsNullOrEmpty(repository)) if (string.IsNullOrEmpty(repository))
{ {
@ -65,17 +57,16 @@ public sealed class TemplateCommandMiddleware(
return; return;
} }
using (var cliLog = new StringLogger(jsonSerializer)) var cliLog = new StringLogger();
try
{ {
try var session = sessionFactory.CreateSession(app);
{
var session = CreateSession(app);
var syncService = await CreateSyncServiceAsync(repository, session); var syncService = await CreateSyncServiceAsync(repository, session);
var syncOptions = new SyncOptions(); var syncOptions = new SyncOptions();
var targets = new ISynchronizer[] var targets = new ISynchronizer[]
{ {
new AppSynchronizer(cliLog), new AppSynchronizer(cliLog),
new AssetFoldersSynchronizer(cliLog), new AssetFoldersSynchronizer(cliLog),
new AssetsSynchronizer(cliLog), new AssetsSynchronizer(cliLog),
@ -83,18 +74,17 @@ public sealed class TemplateCommandMiddleware(
new SchemasSynchronizer(cliLog), new SchemasSynchronizer(cliLog),
new WorkflowsSynchronizer(cliLog), new WorkflowsSynchronizer(cliLog),
new ContentsSynchronizer(cliLog), new ContentsSynchronizer(cliLog),
}; };
foreach (var target in targets) foreach (var target in targets)
{
await target.ImportAsync(syncService, syncOptions, session);
}
}
finally
{ {
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) private static async Task<ISyncService> CreateSyncServiceAsync(string repository, ISession session)
@ -103,27 +93,4 @@ public sealed class TemplateCommandMiddleware(
return new SyncService(fs, session); 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.CodeDom;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Markdig; using Markdig;
using Markdig.Renderers.Normalize; using Markdig.Renderers.Normalize;
using Markdig.Syntax; using Markdig.Syntax;
using Markdig.Syntax.Inlines; using Markdig.Syntax.Inlines;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.ClientLibrary;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Templates; 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 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 sealed class SchemasOptions
{ {
public bool DeletePermanent { get; set; } 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" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ConsoleTables" Version="2.7.0" />
<PackageReference Include="CsvHelper" Version="33.0.1" /> <PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="FFMpegCore" Version="5.1.0" /> <PackageReference Include="FFMpegCore" Version="5.1.0" />
<PackageReference Include="GraphQL" Version="8.2.1" /> <PackageReference Include="GraphQL" Version="8.2.1" />
@ -40,7 +41,7 @@
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Notifo.SDK" Version="1.7.5" /> <PackageReference Include="Notifo.SDK" Version="1.7.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <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" Version="0.4.3" />
<PackageReference Include="YDotNet.Extensions" Version="0.4.3" /> <PackageReference Include="YDotNet.Extensions" Version="0.4.3" />
<PackageReference Include="YDotNet.Server" 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; return typed;
} }
var buffer = await distributedCache.GetAsync(cacheKey, ct); var cached = await distributedCache.GetAsync(cacheKey, ct);
if (cached == null)
if (buffer == null)
{ {
return default!; return default!;
} }
try try
{ {
using (var stream = new MemoryStream(buffer)) using var stream = new MemoryStream(cached);
{ return serializer.Deserialize<T>(stream);
var result = serializer.Deserialize<T>(stream);
return result;
}
} }
catch catch
{ {
@ -83,12 +78,10 @@ public sealed class DefaultDomainObjectCache : IDomainObjectCache
cache.Set(cacheKey, snapshot, cacheOptions.AbsoluteExpirationRelativeToNow!.Value); cache.Set(cacheKey, snapshot, cacheOptions.AbsoluteExpirationRelativeToNow!.Value);
try try
{ {
using (var stream = DefaultPools.MemoryStream.GetStream()) using var stream = DefaultPools.MemoryStream.GetStream();
{ serializer.Serialize(snapshot, stream);
serializer.Serialize(snapshot, stream);
await distributedCache.SetAsync(cacheKey, stream.ToArray(), cacheOptions, ct); await distributedCache.SetAsync(cacheKey, stream.ToArray(), cacheOptions, ct);
}
} }
catch catch
{ {

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

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

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

@ -24,13 +24,13 @@
<PackageReference Include="NodaTime" Version="3.2.0" /> <PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.9.0" /> <PackageReference Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="7.25.0" /> <PackageReference Include="Squidex.Assets" Version="7.26.0" />
<PackageReference Include="Squidex.Caching" Version="7.25.0" /> <PackageReference Include="Squidex.Caching" Version="7.26.0" />
<PackageReference Include="Squidex.Events" Version="7.25.0" /> <PackageReference Include="Squidex.Events" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.25.0" /> <PackageReference Include="Squidex.Hosting.Abstractions" Version="7.26.0" />
<PackageReference Include="Squidex.Log" Version="7.25.0" /> <PackageReference Include="Squidex.Log" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging" Version="7.25.0" /> <PackageReference Include="Squidex.Messaging" Version="7.26.0" />
<PackageReference Include="Squidex.Text" Version="7.25.0" /> <PackageReference Include="Squidex.Text" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.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) public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{ {
return Enumerable.Empty<LocalizedString>(); return [];
} }
public IStringLocalizer WithCulture(CultureInfo culture) 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>() .OfType<XElement>()
.Where(x => x.Name == "response") .Where(x => x.Name == "response")
.Where(x => x.Attribute("code") != null) .Where(x => x.Attribute("code") != null)
?? Enumerable.Empty<XElement>(); ?? [];
foreach (var response in responses) 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 = var context =
new DocumentProcessorContext(document, new DocumentProcessorContext(document,
Enumerable.Empty<Type>(), [],
Enumerable.Empty<Type>(), [],
schemaResolver, schemaResolver,
openApiGenerator, openApiGenerator,
openApiSettings); 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.Schemas;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -31,6 +32,7 @@ public sealed class SchemasController(
ICommandBus commandBus, ICommandBus commandBus,
IContentWorkflow workflow, IContentWorkflow workflow,
IAppProvider appProvider, IAppProvider appProvider,
SchemaAIGenerator schemaAIGenerator,
ScriptingCompleter scriptingCompleter) ScriptingCompleter scriptingCompleter)
: ApiController(commandBus) : ApiController(commandBus)
{ {
@ -105,6 +107,34 @@ public sealed class SchemasController(
return CreatedAtAction(nameof(GetSchema), new { app, schema = request.Name }, response); 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> /// <summary>
/// Update a schema. /// Update a schema.
/// </summary> /// </summary>

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

@ -9,8 +9,9 @@ namespace Squidex.Config.Domain;
public static class ConfigurationExtensions 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.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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.CLI.Commands.Implementation.AI;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Apps.Templates;
@ -103,5 +104,14 @@ public static class ContentsServices
services.AddSingletonAs<TemplatesClient>() services.AddSingletonAs<TemplatesClient>()
.AsSelf(); .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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Text.RegularExpressions;
using Squidex.Log; using Squidex.Log;
namespace Squidex.Config.Startup; namespace Squidex.Config.Startup;
public sealed class LogConfigurationHost(IConfiguration configuration, ISemanticLog log) : IHostedService public sealed class LogConfigurationHost(IConfiguration configuration, ISemanticLog log) : IHostedService
{ {
private const int MaxValueLength = 30;
private static readonly string RedactedValue = "*****"; private static readonly string RedactedValue = "*****";
private static readonly Regex[] SensitivePatterns = private static readonly string[] SensitiveValues =
[ [
// Authentication and API keys "aws",
#pragma warning disable MA0110 // Use the Regex source generator "azure",
new Regex(@"(?i)(secret|token|key|password|credential|auth|api[_-]?key)$"), "bearer",
new Regex(@"(?i)^(aws|azure|google|microsoft|github)[_-]"), "clientid",
new Regex(@"(?i)(jwt|bearer|oauth|saml)"), "credential",
new Regex(@"(?i)(client|secret|password)$"), "database",
"db",
// Connection strings and credentials "github",
new Regex(@"(?i)(connectionstring|connection)$"), "google",
new Regex(@"(?i)(username|password|credential)$"), "jwt",
"key",
// Cloud provider specific "microsoft",
new Regex(@"(?i)(accesskey|secretkey|privatekey|publickey)$"), "pass",
new Regex(@"(?i)(projectid|tenantid)$"), "secret",
"server",
// Database specific "tenant",
new Regex(@"(?i)(mongodb|sqlserver|postgres|mysql)://.*"), "token",
new Regex(@"(?i)(database|db|server|host|port|user|pass)="), "username",
#pragma warning restore MA0110 // Use the Regex source generator
]; ];
public Task StartAsync( public Task StartAsync(
@ -56,14 +55,23 @@ public sealed class LogConfigurationHost(IConfiguration configuration, ISemantic
continue; continue;
} }
var keyLower = key.ToLowerInvariant(); var lowerKey = key.ToLowerInvariant();
if (!logged.Add(lowerKey))
if (logged.Add(keyLower))
{ {
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) 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) 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) => .ConfigureAppConfiguration((hostContext, builder) =>
{ {
builder.ConfigureForSquidex(); builder.ConfigureForSquidex(hostContext.HostingEnvironment);
}) })
.ConfigureServices((context, services) => .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.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.RulesetToEditorconfigConverter" Version="3.3.3" /> <PackageReference Include="Microsoft.CodeAnalysis.RulesetToEditorconfigConverter" Version="3.3.3" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.5" /> <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.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.2.1" /> <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="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.4.1" PrivateAssets="all" /> <PackageReference Include="ReportGenerator" Version="5.4.1" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.Azure" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.FTP" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.ImageSharp" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.S3" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.S3" Version="7.26.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.TusAdapter" Version="7.26.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="21.7.0" /> <PackageReference Include="Squidex.ClientLibrary" Version="21.8.0" />
<PackageReference Include="Squidex.Events.GetEventStore" Version="7.25.0" /> <PackageReference Include="Squidex.Events.GetEventStore" Version="7.26.0" />
<PackageReference Include="Squidex.Hosting" Version="7.25.0" /> <PackageReference Include="Squidex.Hosting" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.All" Version="7.25.0" /> <PackageReference Include="Squidex.Messaging.All" Version="7.26.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.25.0" /> <PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.26.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.4.3" /> <PackageReference Include="YDotNet" Version="0.4.3" />
<PackageReference Include="YDotNet.Native" Version="0.4.3" /> <PackageReference Include="YDotNet.Native" Version="0.4.3" />
@ -83,11 +84,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(IncludeMagick)' == 'true'"> <ItemGroup Condition="'$(IncludeMagick)' == 'true'">
<PackageReference Include="Squidex.Assets.ImageMagick" Version="7.25.0" /> <PackageReference Include="Squidex.Assets.ImageMagick" Version="7.26.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(IncludeKafka)' == 'true'"> <ItemGroup Condition="'$(IncludeKafka)' == 'true'">
<PackageReference Include="Squidex.Messaging.Kafka" Version="7.25.0" /> <PackageReference Include="Squidex.Messaging.Kafka" Version="7.26.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@ -135,6 +136,12 @@
<Folder Include="Areas\Frontend\Resources\" /> <Folder Include="Areas\Frontend\Resources\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Configuration\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="..\..\i18n\frontend_en.json" Link="Areas\Frontend\Resources\frontend_en.json" /> <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" /> <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 = var actual =
new AddSchemaNames(components) new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>()); .ConvertItemAfter(field, source, []);
var expected = var expected =
JsonValue.Object() JsonValue.Object()
@ -196,7 +196,7 @@ public class ValueConvertersTests
var actual = var actual =
new AddSchemaNames(components) new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>()); .ConvertItemAfter(field, source, []);
var expected = source; var expected = source;
@ -221,7 +221,7 @@ public class ValueConvertersTests
var actual = var actual =
new AddSchemaNames(components) new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>()); .ConvertItemAfter(field, source, []);
var expected = source; var expected = source;
@ -245,7 +245,7 @@ public class ValueConvertersTests
var actual = var actual =
new AddSchemaNames(components) new AddSchemaNames(components)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>()); .ConvertItemAfter(field, source, []);
var expected = source; var expected = source;
@ -265,7 +265,7 @@ public class ValueConvertersTests
var actual = var actual =
new AddSchemaNames(ResolvedComponents.Empty) new AddSchemaNames(ResolvedComponents.Empty)
.ConvertItemAfter(field, source, Enumerable.Empty<IField>()); .ConvertItemAfter(field, source, []);
var expected = 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() public SubscriptionPublisherTests()
{ {
sut = new SubscriptionPublisher(subscriptionService, Enumerable.Empty<ISubscriptionEventCreator>()); sut = new SubscriptionPublisher(subscriptionService, []);
} }
[Fact] [Fact]

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

@ -58,7 +58,7 @@ public class ConfigPlansProviderTests
[InlineData("my-plan")] [InlineData("my-plan")]
public void Should_return_infinite_if_nothing_configured(string? planId) 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); var actual = sut.GetActualPlan(planId);
@ -78,7 +78,7 @@ public class ConfigPlansProviderTests
[Fact] [Fact]
public void Should_return_infinite_plan_for_free_plan_if_not_found() 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(); 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 source = CreateContent();
var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), AppProvider); var sut = new ContentEnricher([], AppProvider);
var actual = await sut.EnrichAsync(source, true, ApiContext, CancellationToken); var actual = await sut.EnrichAsync(source, true, ApiContext, CancellationToken);
@ -113,7 +113,7 @@ public class ContentEnricherTests : GivenContext
{ {
var source = CreateContent(); 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); 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; get => persistenceFactory;
} }
public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = Enumerable.Empty<Envelope<IEvent>>(); public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = [];
protected HandlerTestBase() protected HandlerTestBase()
{ {
@ -41,7 +41,7 @@ public abstract class HandlerTestBase<TState> : GivenContext
.Invokes((IReadOnlyList<Envelope<IEvent>> events, CancellationToken _) => LastEvents = events); .Invokes((IReadOnlyList<Envelope<IEvent>> events, CancellationToken _) => LastEvents = events);
A.CallTo(() => persistence.DeleteAsync(CancellationToken)) A.CallTo(() => persistence.DeleteAsync(CancellationToken))
.Invokes(() => LastEvents = Enumerable.Empty<Envelope<IEvent>>()); .Invokes(() => LastEvents = []);
#pragma warning restore MA0056 // Do not call overridable members in constructor #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'); const CopyPlugin = require('copy-webpack-plugin');
class FilterSassWarningsPlugin { class FilterSassWarningsPlugin {
apply(compiler) { apply(compiler) {
compiler.hooks.done.tap('FilterSassWarningsPlugin', (stats) => { compiler.hooks.done.tap('FilterSassWarningsPlugin', (stats) => {
stats.compilation.warnings = stats.compilation.warnings.filter( stats.compilation.warnings = stats.compilation.warnings.filter(warning => {
(warning) => { const message = warning.message || warning.toString();
const message = warning.message || warning.toString(); return !message.includes('sass-loader');
return !message.includes('sass-loader');
}
);
}); });
} });
} }
}
module.exports = { module.exports = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],

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

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

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

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

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

@ -1,116 +1,169 @@
<form [formGroup]="createForm.form" (ngSubmit)="createSchema()"> <form [formGroup]="actualForm.form" (ngSubmit)="createSchema()">
<sqx-modal-dialog (dialogClose)="emitClose()" tourId="schemaForm"> <sqx-modal-dialog (dialogClose)="emitClose()" hasTabs="true" size="lg" tourId="schemaForm">
<ng-container title> <ng-container title>
@if (import) { @if (source) {
{{ "schemas.clone" | sqxTranslate }} {{ "schemas.clone" | sqxTranslate }}
} @else { } @else {
{{ "schemas.create" | sqxTranslate }} {{ "schemas.create" | sqxTranslate }}
} }
</ng-container> </ng-container>
<ng-container content> <ng-container tabs>
<sqx-form-error [error]="createForm.error | async" /> <div class="row align-items-center">
<div class="form-group"> <div class="col">
<label for="name"> <ul class="nav nav-tabs2">
{{ "common.name" | sqxTranslate }} <small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small> <li class="nav-item">
</label> <a class="nav-link" [class.active]="selectedTab === 0" (click)="selectTab(0)">
<sqx-control-errors for="name" /> {{ "schemas.createCustom" | sqxTranslate }}
<input class="form-control" id="name" autocomplete="off" formControlName="name" sqxFocusOnInit sqxTransformInput="LowerCase" /> </a>
<sqx-form-hint> {{ "schemas.schemaNameHint" | sqxTranslate }} </sqx-form-hint> </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> </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"> @if (selectedTab === 0) {
<div class="row"> <div class="form-group mt-4">
<div class="col-6 type"> <div class="row">
<label> <div class="col-12 col-md-4 type">
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Default'" /> <label>
<div class="row g-0"> <input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Default'" />
<div class="col-auto"> <div class="row flex-nowrap g-0">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Default'"> <div class="col-auto">
<i class="icon-multiple-content"></i> <div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Default'">
<i class="icon-multiple-content"></i>
</div>
</div> </div>
</div>
<div class="col"> <div class="col">
<div class="type-title">{{ "schemas.modeMultiple" | sqxTranslate }}</div> <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>
</div> </label>
</label> </div>
</div>
<div class="col-6 type"> <div class="col-12 col-md-4 type">
<label> <label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Singleton'" /> <input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Singleton'" />
<div class="row g-0"> <div class="row flex-nowrap g-0">
<div class="col-auto"> <div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Singleton'"> <div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Singleton'">
<i class="icon-single-content"></i> <i class="icon-single-content"></i>
</div>
</div> </div>
</div>
<div class="col"> <div class="col">
<div class="type-title">{{ "schemas.modeSingle" | sqxTranslate }}</div> <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>
</div> </label>
</label> </div>
</div>
<div class="col-6 type"> <div class="col-12 col-md-4 type">
<label> <label>
<input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Component'" /> <input class="radio-input" formControlName="type" name="type" type="radio" [value]="'Component'" />
<div class="row g-0"> <div class="row flex-nowrap g-0">
<div class="col-auto"> <div class="col-auto">
<div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Component'"> <div class="btn-radio" [class.active]="createForm.form.controls['type'].value === 'Component'">
<i class="icon-component"></i> <i class="icon-component"></i>
</div>
</div> </div>
</div>
<div class="col"> <div class="col">
<div class="type-title">{{ "schemas.modeComponent" | sqxTranslate }}</div> <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>
</div> </label>
</label> </div>
</div> </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"> @if (schemasState.categoryNames | async; as categories) {
<button class="btn btn-sm btn-text-secondary" [class.hidden]="showImport" (click)="toggleImport()" type="button"> @if (categories.length > 0) {
{{ "schemas.import" | sqxTranslate }} <div class="form-group">
</button> <label for="category">{{ "common.category" | sqxTranslate }}</label>
<button class="btn btn-sm btn-text-secondary force" [class.hidden]="!showImport" (click)="toggleImport()" type="button"> <select class="form-select" id="category" formControlName="initialCategory">
{{ "common.hide" | sqxTranslate }} <option></option>
</button>
@if (showImport) { @for (category of categories; track category) {
<sqx-code-editor formControlName="importing" [height]="250" valueMode="Json" /> <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>
<ng-container footer> <ng-container footer>
<button class="btn btn-text-secondary" (click)="dialogClose.emit()" type="button"> <button class="btn btn-text-secondary" (click)="dialogClose.emit()" type="button">
{{ "common.cancel" | sqxTranslate }} {{ "common.cancel" | sqxTranslate }}
</button> </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> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>
</form> </form>

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

@ -6,9 +6,10 @@
*/ */
import { AsyncPipe } from '@angular/common'; 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 { 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({ @Component({
selector: 'sqx-schema-form', selector: 'sqx-schema-form',
@ -23,6 +24,7 @@ import { ApiUrlConfig, AppsState, CodeEditorComponent, ControlErrorsComponent, C
FormErrorComponent, FormErrorComponent,
FormHintComponent, FormHintComponent,
FormsModule, FormsModule,
LoaderComponent,
ModalDialogComponent, ModalDialogComponent,
ReactiveFormsModule, ReactiveFormsModule,
TooltipDirective, TooltipDirective,
@ -31,6 +33,8 @@ import { ApiUrlConfig, AppsState, CodeEditorComponent, ControlErrorsComponent, C
], ],
}) })
export class SchemaFormComponent implements OnInit { export class SchemaFormComponent implements OnInit {
public readonly hasChatBot = inject(UIOptions).value.canUseChatBot;
@Output() @Output()
public create = new EventEmitter<SchemaDto>(); public create = new EventEmitter<SchemaDto>();
@ -38,29 +42,40 @@ export class SchemaFormComponent implements OnInit {
public dialogClose = new EventEmitter(); public dialogClose = new EventEmitter();
@Input() @Input()
public import: any; public source: any;
public createForm = new CreateSchemaForm(); 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( constructor(
public readonly apiUrl: ApiUrlConfig, public readonly apiUrl: ApiUrlConfig,
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
private readonly schemasService: SchemasService,
) { ) {
} }
public ngOnInit() { 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() { public selectTab(tab: number) {
this.showImport = !this.showImport; this.selectedTab = tab;
this.source = null;
return false; const { type, name } = this.createForm.form.value;
this.createForm.load({ type, name });
} }
public emitCreate(value: SchemaDto) { public emitCreate(value: SchemaDto) {
@ -72,6 +87,58 @@ export class SchemaFormComponent implements OnInit {
} }
public createSchema() { 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(); const value = this.createForm.submit();
if (!value) { if (!value) {
return; return;

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

@ -41,5 +41,5 @@
</form> </form>
</ng-container> </ng-container>
</sqx-layout> </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 /> <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); return getCategoryTree(schemas, categories, filter);
}); });
public import: any; public source: any;
constructor( constructor(
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
@ -68,7 +68,7 @@ export class SchemasPageComponent implements OnInit {
this.subscriptions.add( this.subscriptions.add(
this.messageBus.of(SchemaCloning) this.messageBus.of(SchemaCloning)
.subscribe(event => { .subscribe(event => {
this.import = event.schema; this.source = event.schema;
this.addSchemaDialog.show(); this.addSchemaDialog.show();
})); }));
@ -105,8 +105,8 @@ export class SchemasPageComponent implements OnInit {
this.addSchemaDialog.hide(); this.addSchemaDialog.hide();
} }
public createSchema(importing: any = null) { public createSchema(source: any = null) {
this.import = importing; this.source = source;
this.addSchemaDialog.show(); 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 }) @Input({ required: true })
public for!: string | AbstractControl; public for!: string | AbstractControl;
@Input()
public submitCount: null | number | undefined;
@Input() @Input()
public fieldName: string | null | undefined; public fieldName: string | null | undefined;
@ -100,7 +103,10 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
private createMessages() { private createMessages() {
const errorMessages: string[] = []; 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)) { for (const [key, error] of Object.entries(this.control.errors)) {
const message = formatError(this.localizer, this.controlDisplayName, key, error, this.control.value); 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 { .ace_gutter-active-line {
background: color.adjust($color-border, $lightness: -5%) !important; 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() @Input()
public valueMode: 'String' | 'Json' | 'JsonString' = 'String'; public valueMode: 'String' | 'Json' | 'JsonString' = 'String';
@Input({ transform: booleanAttribute })
public disableTools = false;
@Input({ transform: numberAttribute }) @Input({ transform: numberAttribute })
public maxLines: number | undefined; public maxLines: number | undefined;
@ -96,7 +99,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
this.setMode(); 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(); this.setOptions();
} }
@ -166,7 +169,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
this.setWordWrap(); this.setWordWrap();
this.onDisabled(this.snapshot.isDisabled); this.onDisabled(this.snapshot.isDisabled);
if (this.aceTools) { if (this.aceTools && !this.disableTools) {
const originalCompleter = this.aceEditor.completer!; const originalCompleter = this.aceEditor.completer!;
const originalShowDocTooltip = originalCompleter.showDocTooltip; const originalShowDocTooltip = originalCompleter.showDocTooltip;
originalCompleter.showDocTooltip = item => { originalCompleter.showDocTooltip = item => {
@ -205,9 +208,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
const previous = this.aceEditor.completers || []; const previous = this.aceEditor.completers || [];
this.aceEditor.completers = [...previous, completer]; this.aceEditor.completers = [...previous, completer];
} }
this.aceEditor.on('blur', () => { this.aceEditor.on('blur', () => {
this.changeValue(); this.changeValue();
this.callTouched(); this.callTouched();
}); });
@ -310,8 +313,13 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
minLines, minLines,
printMargin: !this.singleLine, printMargin: !this.singleLine,
showGutter: !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); 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" [disabled]="disabled"
[height]="height" [height]="height"
[maxLines]="maxLines" [maxLines]="maxLines"
[mode]="mode"
(ngModelChange)="change($event)" (ngModelChange)="change($event)"
[ngModel]="ngModel" [ngModel]="ngModel"
[singleLine]="singleLine" [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 = { export const SingleLine: Story = {
render: args => ({ render: args => ({
props: 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 { public submit(): TOut | null {
this.updateSubmitState(null, true);
this.form.markAllAsTouched(); this.form.markAllAsTouched();
if (!hasNonCustomError(this.form)) { if (!hasNonCustomError(this.form)) {
const value = this.transformSubmit(this.form.value); const value = this.transformSubmit(this.form.value);
if (value) { if (value) {
this.updateSubmitState(null, true);
this.disable(); 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"> <div class="notification-container notification-container-bottom-right">
@for (notification of snapshot.notifications; track notification) { @for (notification of snapshot.notifications; track notification) {
<div class="alert alert-light alert-dismissible border shadow-sm timed" @fade (click)="close(notification)" role="alert"> <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 class="timer timer-{{ notification.messageType }}"></div>
<div [sqxMarkdown]="notification.message | sqxTranslate"></div> <div [sqxMarkdown]="notification.message | sqxTranslate"></div>
</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) { public setDisabledState(isDisabled: boolean) {
this.next({ isDisabled } as any); this.next({ isDisabled } as any);
this.onDisabled(this.snapshot.isDisabled); 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="grid" style="--bs-gap: 0.5rem 0.5rem">
<div <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" [class.border-primary]="!template"
(click)="selectTemplate()" (click)="selectTemplate()"
data-testid="new-app" data-testid="new-app"
@ -45,7 +45,7 @@
@for (availableTemplate of templates; track availableTemplate) { @for (availableTemplate of templates; track availableTemplate) {
<div <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" [class.border-primary]="availableTemplate === template"
(click)="selectTemplate(availableTemplate)"> (click)="selectTemplate(availableTemplate)">
<div class="card-body"> <div class="card-body">

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

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

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

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

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

@ -5724,6 +5724,154 @@ export interface IUpsertSchemaNestedFieldDto {
readonly properties: FieldPropertiesDto; 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 { export class UpdateSchemaDto implements IUpdateSchemaDto {
/** Uses the cache values because the actual object is frozen. */ /** Uses the cache values because the actual object is frozen. */
private readonly cachedValues: { [key: string]: any } = {}; private readonly cachedValues: { [key: string]: any } = {};
@ -9811,14 +9959,15 @@ export interface IFlowExecutionStepStateDto {
readonly attempts: FlowExecutionStepAttemptDto[]; 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> = [ export const FlowExecutionStatusValues: ReadonlyArray<FlowExecutionStatus> = [
"Pending", "Pending",
"Scheduled", "Scheduled",
"Completed", "Completed",
"Failed", "Failed",
"Running" "Running",
"Cancelled"
]; ];
export class FlowExecutionStepAttemptDto implements IFlowExecutionStepAttemptDto { 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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/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', () => { describe('SchemasService', () => {
const version = new VersionTag('1'); const version = new VersionTag('1');
@ -104,6 +104,25 @@ describe('SchemasService', () => {
expect(schema!).toEqual(createSchema(12)); 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', it('should make put request to update schema',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateSchemaDto({ label: 'label1' }); 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 { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ApiUrlConfig, HTTP, pretifyError, Resource, ScriptCompletions, Versioned, VersionOrTag } from '@app/framework'; 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'; import { QueryModel } from './query';
@Injectable({ @Injectable({
@ -53,6 +53,16 @@ export class SchemasService {
pretifyError('i18n:schemas.createFailed')); 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> { public putScripts(appName: string, resource: Resource, dto: Record<string, string>, version: VersionOrTag): Observable<SchemaDto> {
const link = resource._links['update/scripts']; 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) { public transformSubmit(value: any) {
const { name, type, importing, initialCategory } = value; 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)); 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) { private replaceSchema(schema: SchemaDto, oldVersion?: Version, updateText?: string) {
if (!oldVersion || oldVersion !== schema.version) { if (!oldVersion || oldVersion !== schema.version) {
if (updateText) { if (updateText) {

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

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

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

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

5
tools/.editorconfig

@ -58,7 +58,7 @@ dotnet_diagnostic.MA0004.severity = none
dotnet_diagnostic.MA0006.severity = none dotnet_diagnostic.MA0006.severity = none
# MA0007: Add a comma after the last value # MA0007: Add a comma after the last value
dotnet_diagnostic.MA0007.severity = none dotnet_diagnostic.MA0007.severity = warning
# MA0008: Add StructLayoutAttribute # MA0008: Add StructLayoutAttribute
dotnet_diagnostic.MA0008.severity = none dotnet_diagnostic.MA0008.severity = none
@ -183,5 +183,8 @@ dotnet_diagnostic.SA1615.severity = none
# SA1623: Property summary documentation should match accessors # SA1623: Property summary documentation should match accessors
dotnet_diagnostic.SA1623.severity = none 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 # 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 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. // STEP 2: Make the client anonymous.
var clientRequest = new UpdateClientDto var clientRequest = new UpdateClientDto
{ {
AllowAnonymous = true AllowAnonymous = true,
}; };
await app.Apps.PutClientAsync("default", clientRequest); await app.Apps.PutClientAsync("default", clientRequest);
@ -65,7 +65,7 @@ public class AnonymousTests(ClientFixture fixture) : IClassFixture<ClientFixture
// STEP 2: Make the client anonymous. // STEP 2: Make the client anonymous.
var clientRequest = new UpdateClientDto var clientRequest = new UpdateClientDto
{ {
AllowAnonymous = true AllowAnonymous = true,
}; };
await app.Apps.PutClientAsync("default", clientRequest); await app.Apps.PutClientAsync("default", clientRequest);
@ -76,7 +76,7 @@ public class AnonymousTests(ClientFixture fixture) : IClassFixture<ClientFixture
{ {
Name = "my-content", Name = "my-content",
// Schema must be published to create content. // Schema must be published to create content.
IsPublished = true IsPublished = true,
}; };
await app.Schemas.PostSchemaAsync(schemaRequest); 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, AllowAnonymous = true,
ApiCallsLimit = 100, ApiCallsLimit = 100,
ApiTrafficLimit = 200, ApiTrafficLimit = 200,
Role = "Owner" Role = "Owner",
}; };
var clients_2 = await app.Apps.PutClientAsync(client.Id, updateNameRequest); 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 var createRequest = new CreateClientDto
{ {
Id = id Id = id,
}; };
var clients = await app.Apps.PostClientAsync(createRequest); 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. // STEP 1: Do not invite contributors when flag is false.
var createRequest = new AssignContributorDto var createRequest = new AssignContributorDto
{ {
ContributorId = "test@squidex.io" ContributorId = "test@squidex.io",
}; };
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => var ex = await Assert.ThrowsAnyAsync<SquidexException>(() =>
@ -72,7 +72,7 @@ public sealed class AppContributorsTests(ClientFixture fixture) : IClassFixture<
{ {
ContributorId = contributor.ContributorId, ContributorId = contributor.ContributorId,
// Test update of role. // Test update of role.
Role = "Owner" Role = "Owner",
}; };
var contributors_2 = await app.Apps.PostContributorAsync(updateRequest); 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 must be true, otherwise new users are not created.
Invite = true, Invite = true,
// The initial role or editor otherwise. // The initial role or editor otherwise.
Role = role Role = role,
}; };
var contributors = await app.Apps.PostContributorAsync(createInviteRequest); 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, Name = appName,
// The template is just referenced by the name. // The template is just referenced by the name.
Template = template.Name Template = template.Name,
}; };
var (app, _) = await _.PostAppAsync(createRequest); 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 = Fallback =
[ [
"it" "it",
], ],
IsOptional = true IsOptional = true,
}; };
var languages_2 = await app.Apps.PutLanguageAsync("de", updateRequest); var languages_2 = await app.Apps.PutLanguageAsync("de", updateRequest);
@ -101,9 +101,9 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
{ {
Fallback = Fallback =
[ [
"de" "de",
], ],
IsOptional = true IsOptional = true,
}; };
await app.Apps.PutLanguageAsync("it", updateRequest); 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. // STEP 3: Change master language to Italian.
var masterRequest = new UpdateLanguageDto var masterRequest = new UpdateLanguageDto
{ {
IsMaster = true IsMaster = true,
}; };
var languages_4 = await app.Apps.PutLanguageAsync("it", masterRequest); var languages_4 = await app.Apps.PutLanguageAsync("it", masterRequest);
@ -150,9 +150,9 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
{ {
Fallback = Fallback =
[ [
"de" "de",
], ],
IsOptional = true IsOptional = true,
}; };
await app.Apps.PutLanguageAsync("it", updateRequest); await app.Apps.PutLanguageAsync("it", updateRequest);
@ -173,7 +173,7 @@ public sealed class AppLanguagesTests(ClientFixture fixture) : IClassFixture<Cli
{ {
var createRequest = new AddLanguageDto var createRequest = new AddLanguageDto
{ {
Language = code Language = code,
}; };
await app.Apps.PostLanguageAsync(createRequest); 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.. // STEP 2: Update role..
var updateRequest = new UpdateRoleDto var updateRequest = new UpdateRoleDto
{ {
Permissions = ["a", "b"] Permissions = ["a", "b"],
}; };
var roles_2 = await _.Client.Apps.PutRoleAsync(roleName, updateRequest); 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. // STEP 2 Assign client and contributor.
var createClientRequest = new CreateClientDto var createClientRequest = new CreateClientDto
{ {
Id = client Id = client,
}; };
await _.Client.Apps.PostClientAsync(createClientRequest); await _.Client.Apps.PostClientAsync(createClientRequest);
@ -155,7 +155,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
// Test diffferent role names. // Test diffferent role names.
Role = role, Role = role,
// Invite must be true, otherwise new users are not created. // Invite must be true, otherwise new users are not created.
Invite = true Invite = true,
}; };
await _.Client.Apps.PostContributorAsync(assignRequest); await _.Client.Apps.PostContributorAsync(assignRequest);
@ -165,7 +165,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
{ {
var updateRequest = new UpdateClientDto var updateRequest = new UpdateClientDto
{ {
Role = role Role = role,
}; };
await _.Client.Apps.PutClientAsync(client, updateRequest); await _.Client.Apps.PutClientAsync(client, updateRequest);
@ -175,7 +175,7 @@ public sealed class AppRolesTests(CreatedAppFixture fixture) : IClassFixture<Cre
{ {
var createRequest = new AddRoleDto var createRequest = new AddRoleDto
{ {
Name = name Name = name,
}; };
var roles = await _.Client.Apps.PostRoleAsync(createRequest); 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. // STEP 1: Update app.
var updateRequest = new UpdateAppDto var updateRequest = new UpdateAppDto
{ {
Label = Guid.NewGuid().ToString() Label = Guid.NewGuid().ToString(),
}; };
var app_1 = await _.Client.Apps.PutAppAsync(updateRequest); var app_1 = await _.Client.Apps.PutAppAsync(updateRequest);
@ -46,7 +46,7 @@ public sealed class AppTests(CreatedAppFixture fixture) : IClassFixture<CreatedA
// STEP 1: Update app. // STEP 1: Update app.
var updateRequest = new UpdateAppDto var updateRequest = new UpdateAppDto
{ {
Description = Guid.NewGuid().ToString() Description = Guid.NewGuid().ToString(),
}; };
var app_1 = await _.Client.Apps.PutAppAsync(updateRequest); var app_1 = await _.Client.Apps.PutAppAsync(updateRequest);
@ -125,12 +125,12 @@ public sealed class AppTests(CreatedAppFixture fixture) : IClassFixture<CreatedA
{ {
Patterns = Patterns =
[ [
new PatternDto { Name = "pattern", Regex = ".*" } new PatternDto { Name = "pattern", Regex = ".*" },
], ],
Editors = 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); 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> Transitions = new Dictionary<string, WorkflowTransitionDto>
{ {
["Published"] = new WorkflowTransitionDto() ["Published"] = new WorkflowTransitionDto(),
} },
}, },
["Published"] = new WorkflowStepDto(), ["Published"] = new WorkflowStepDto(),
}, },
Name = workflowName Name = workflowName,
}; };
var workflows_2 = await app.Apps.PutWorkflowAsync(workflow.Id, updateRequest); 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 var createRequest = new AddWorkflowDto
{ {
Name = workflowName Name = workflowName,
}; };
var workflows = await app.Apps.PostWorkflowAsync(createRequest); 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 // STEP 2: Update folder
var updateRequest = new RenameAssetFolderDto var updateRequest = new RenameAssetFolderDto
{ {
FolderName = Guid.NewGuid().ToString() FolderName = Guid.NewGuid().ToString(),
}; };
var folder_1 = await _.Client.Assets.PutAssetFolderAsync(folder_0.Id, updateRequest); 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 // STEP 2: Update folder
var moveRequest = new MoveAssetFolderDto var moveRequest = new MoveAssetFolderDto
{ {
ParentId = folder1.Id ParentId = folder1.Id,
}; };
var folder2_1 = await _.Client.Assets.PutAssetFolderParentAsync(folder2.Id, moveRequest); 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 // STEP 2: Update folder
var moveRequest = new MoveAssetFolderDto var moveRequest = new MoveAssetFolderDto
{ {
ParentId = folder2.Id ParentId = folder2.Id,
}; };
await Assert.ThrowsAnyAsync<SquidexException>(() => _.Client.Assets.PutAssetFolderParentAsync(folder1.Id, moveRequest)); await Assert.ThrowsAnyAsync<SquidexException>(() => _.Client.Assets.PutAssetFolderParentAsync(folder1.Id, moveRequest));
@ -156,7 +156,7 @@ public class AssetFoldersTests(CreatedAppFixture fixture) : IClassFixture<Create
{ {
FolderName = name, FolderName = name,
// Create a nested asset folder. // Create a nested asset folder.
ParentId = parentId ParentId = parentId,
}; };
var folder = await _.Client.Assets.PostAssetFolderAsync(createRequest); 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 = @" Create = @"
if (ctx.command.mimeType == 'image/jpeg') { if (ctx.command.mimeType == 'image/jpeg') {
disallow('We do not use jpeg anymore.'); disallow('We do not use jpeg anymore.');
}" }",
}; };
await client.Apps.PutAssetScriptsAsync(scriptRequest); await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -52,7 +52,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
ctx.command.metadata['key1'] = 'value1'; ctx.command.metadata['key1'] = 'value1';
ctx.command.metadata['key2'] = 'value2'; ctx.command.metadata['key2'] = 'value2';
ctx.command.tags.add('tag1'); ctx.command.tags.add('tag1');
ctx.command.tags.add('tag2');" ctx.command.tags.add('tag2');",
}; };
await client.Apps.PutAssetScriptsAsync(scriptRequest); await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -78,7 +78,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
Update = @" Update = @"
if (ctx.command.mimeType == 'image/jpeg') { if (ctx.command.mimeType == 'image/jpeg') {
disallow('We do not use jpeg anymore.'); disallow('We do not use jpeg anymore.');
}" }",
}; };
await client.Apps.PutAssetScriptsAsync(scriptRequest); await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -104,7 +104,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
{ {
Update = @" Update = @"
ctx.command.metadata['key1'] = 'value1'; ctx.command.metadata['key1'] = 'value1';
ctx.command.metadata['key2'] = 'value2';" ctx.command.metadata['key2'] = 'value2';",
}; };
await client.Apps.PutAssetScriptsAsync(scriptRequest); await client.Apps.PutAssetScriptsAsync(scriptRequest);
@ -132,7 +132,7 @@ public class AssetScriptingTests(ClientFixture fixture) : IClassFixture<ClientFi
Query = @" Query = @"
if (ctx.asset.mimeType == 'image/jpeg') { if (ctx.asset.mimeType == 'image/jpeg') {
disallow('We do not use jpeg anymore.'); disallow('We do not use jpeg anymore.');
}" }",
}; };
await client.Apps.PutAssetScriptsAsync(scriptRequest); await client.Apps.PutAssetScriptsAsync(scriptRequest);

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

@ -250,8 +250,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Tags = Tags =
[ [
randomTag1, randomTag1,
randomTag2 randomTag2,
] ],
}; };
await _.Client.Assets.PutAssetAsync(asset_1.Id, randomMetadataRequest, ct); await _.Client.Assets.PutAssetAsync(asset_1.Id, randomMetadataRequest, ct);
@ -272,8 +272,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Tags = Tags =
[ [
tag1, tag1,
tag2 tag2,
] ],
}; };
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, metadataRequest); 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. // STEP 2: Annotate file name.
var fileNameRequest = new AnnotateAssetDto var fileNameRequest = new AnnotateAssetDto
{ {
FileName = "My Image" FileName = "My Image",
}; };
await _.Client.Assets.AnnotateAsync(asset_1, fileNameRequest, strategy); await _.Client.Assets.AnnotateAsync(asset_1, fileNameRequest, strategy);
@ -333,8 +333,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Metadata = new Dictionary<string, object> Metadata = new Dictionary<string, object>
{ {
["pw"] = 100L, ["pw"] = 100L,
["ph"] = 20L ["ph"] = 20L,
} },
}; };
await _.Client.Assets.AnnotateAsync(asset_1, metadataRequest, strategy); await _.Client.Assets.AnnotateAsync(asset_1, metadataRequest, strategy);
@ -359,7 +359,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 2: Annotate slug. // STEP 2: Annotate slug.
var slugRequest = new AnnotateAssetDto var slugRequest = new AnnotateAssetDto
{ {
Slug = "my-image" Slug = "my-image",
}; };
await _.Client.Assets.AnnotateAsync(asset_1, slugRequest, strategy); await _.Client.Assets.AnnotateAsync(asset_1, slugRequest, strategy);
@ -387,8 +387,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Tags = Tags =
[ [
"tag1", "tag1",
"tag2" "tag2",
] ],
}; };
await _.Client.Assets.AnnotateAsync(asset_1, tagsRequest, strategy); await _.Client.Assets.AnnotateAsync(asset_1, tagsRequest, strategy);
@ -423,7 +423,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 4: Protect asset. // STEP 4: Protect asset.
var protectRequest = new AnnotateAssetDto var protectRequest = new AnnotateAssetDto
{ {
IsProtected = true IsProtected = true,
}; };
await _.Client.Assets.AnnotateAsync(asset_1, protectRequest, strategy); await _.Client.Assets.AnnotateAsync(asset_1, protectRequest, strategy);
@ -484,7 +484,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 1: Create folder. // STEP 1: Create folder.
var folderRequest = new CreateAssetFolderDto var folderRequest = new CreateAssetFolderDto
{ {
FolderName = "folder" FolderName = "folder",
}; };
var folder = await app.Assets.PostAssetFolderAsync(folderRequest); var folder = await app.Assets.PostAssetFolderAsync(folderRequest);
@ -510,7 +510,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
Query = $@" Query = $@"
if (ctx.assetId === '{asset_1.Id}') {{ if (ctx.assetId === '{asset_1.Id}') {{
disallow(); disallow();
}}" }}",
}; };
await app.Apps.PutAssetScriptsAsync(scriptsRequest); await app.Apps.PutAssetScriptsAsync(scriptsRequest);
@ -541,7 +541,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 1: Create folder. // STEP 1: Create folder.
var folderRequest = new CreateAssetFolderDto var folderRequest = new CreateAssetFolderDto
{ {
FolderName = "folder" FolderName = "folder",
}; };
var folder = await app.Assets.PostAssetFolderAsync(folderRequest); var folder = await app.Assets.PostAssetFolderAsync(folderRequest);
@ -561,7 +561,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
getAssetBlurHash(ctx.asset, function (hash) { getAssetBlurHash(ctx.asset, function (hash) {
ctx.command.metadata['blurHash'] = hash; ctx.command.metadata['blurHash'] = hash;
}); });
}" }",
}; };
await app.Apps.PutAssetScriptsAsync(scriptsRequest); await app.Apps.PutAssetScriptsAsync(scriptsRequest);
@ -592,7 +592,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
var assets_1 = await _.Client.Assets.GetAssetsAsync( var assets_1 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery new AssetQuery
{ {
Filter = "metadata/pixelWidth eq 600" Filter = "metadata/pixelWidth eq 600",
}); });
Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id); 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, asset_1.Id,
new AnnotateAssetDto 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( var assets_2 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery new AssetQuery
{ {
Filter = "metadata/custom eq 'foo'" Filter = "metadata/custom eq 'foo'",
}); });
Assert.Contains(assets_2.Items, x => x.Id == asset_1.Id); 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( var assets_1 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery new AssetQuery
{ {
ParentId = Guid.Empty.ToString() ParentId = Guid.Empty.ToString(),
}); });
Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id); 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. // STEP 1: Create asset folder.
var folderRequest = new CreateAssetFolderDto var folderRequest = new CreateAssetFolderDto
{ {
FolderName = "sub" FolderName = "sub",
}; };
var folder = await _.Client.Assets.PostAssetFolderAsync(folderRequest); 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( var assets_1 = await _.Client.Assets.GetAssetsAsync(
new AssetQuery new AssetQuery
{ {
ParentId = folder.Id ParentId = folder.Id,
}); });
Assert.Single(assets_1.Items, x => x.Id == asset_1.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. // STEP 1: Create asset folder.
var createRequest1 = new CreateAssetFolderDto var createRequest1 = new CreateAssetFolderDto
{ {
FolderName = "folder1" FolderName = "folder1",
}; };
var folder_1 = await _.Client.Assets.PostAssetFolderAsync(createRequest1); var folder_1 = await _.Client.Assets.PostAssetFolderAsync(createRequest1);
@ -679,7 +679,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
{ {
FolderName = "subfolder", FolderName = "subfolder",
// Reference the parent folder by Id, so it must exist first. // 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); 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( var deleted = await _.Client.Assets.GetAssetsAsync(
new AssetQuery new AssetQuery
{ {
Filter = "isDeleted eq true" Filter = "isDeleted eq true",
}); });
var permanent = strategy is ContentStrategies.Deletion.SinglePermanent or ContentStrategies.Deletion.BulkPermanent; var permanent = strategy is ContentStrategies.Deletion.SinglePermanent or ContentStrategies.Deletion.BulkPermanent;
@ -791,8 +791,8 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
path = "isDeleted", path = "isDeleted",
op = "eq", op = "eq",
value = true, value = true,
} },
} },
}); });
Assert.NotEmpty(assets.Items); Assert.NotEmpty(assets.Items);
@ -825,7 +825,7 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFix
// STEP 2: Rename tag. // STEP 2: Rename tag.
var renameRequest = new RenameTagDto var renameRequest = new RenameTagDto
{ {
TagName = "pngs" TagName = "pngs",
}; };
await client.Assets.PutTagAsync("type/png", renameRequest); 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, Url = uri,
// Choose a new app name, because the old one is deleted. // Choose a new app name, because the old one is deleted.
Name = appNameRestore Name = appNameRestore,
}; };
await _.Client.Backups.PostRestoreJobAsync(restoreRequest); await _.Client.Backups.PostRestoreJobAsync(restoreRequest);
@ -115,7 +115,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
{ {
Url = uri, Url = uri,
// Restore the old app name, because it has been deleted anyway. // Restore the old app name, because it has been deleted anyway.
Name = appName Name = appName,
}; };
await app.Backups.PostRestoreJobAsync(restoreRequest); await app.Backups.PostRestoreJobAsync(restoreRequest);
@ -137,7 +137,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
await contents.CreateAsync( await contents.CreateAsync(
new TestEntityData new TestEntityData
{ {
Number = 1 Number = 1,
}); });
@ -155,7 +155,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
// Create a workflow // Create a workflow
var workflowRequest = new AddWorkflowDto var workflowRequest = new AddWorkflowDto
{ {
Name = "workflow" Name = "workflow",
}; };
await app.Apps.PostWorkflowAsync(workflowRequest); await app.Apps.PostWorkflowAsync(workflowRequest);
@ -164,7 +164,7 @@ public class BackupTests(ClientFixture fixture) : IClassFixture<ClientFixture>
// Create a language // Create a language
var languageRequest = new AddLanguageDto var languageRequest = new AddLanguageDto
{ {
Language = "de" Language = "de",
}; };
await app.Apps.PostLanguageAsync(languageRequest); 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( var content_1 = await contents.CreateAsync(
new TestEntityData new TestEntityData
{ {
String = "hello" String = "hello",
}); });
Assert.Equal("hello", content_1.Data.String); Assert.Equal("hello", content_1.Data.String);
@ -48,7 +48,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
content_1.Id, content_1.Id,
new ChangeStatus new ChangeStatus
{ {
Status = "Published" Status = "Published",
}); });
// Should not return deleted field. // Should not return deleted field.
@ -68,7 +68,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
var contentA_1 = await contents.CreateAsync( var contentA_1 = await contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = null References = null,
}); });
@ -76,7 +76,7 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture<Crea
var contentB_1 = await contents.CreateAsync( var contentB_1 = await contents.CreateAsync(
new TestEntityWithReferencesData 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, var contentB_2 = await contents.ChangeStatusAsync(contentB_1.Id,
new ChangeStatus new ChangeStatus
{ {
Status = "Published" Status = "Published",
}); });
// Should not return deleted field. // 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 new UpsertSchemaFieldDto
{ {
Name = SimpleEntityData.StringField, Name = SimpleEntityData.StringField,
Properties = new StringFieldPropertiesDto() Properties = new StringFieldPropertiesDto(),
}, },
], ],
IsPublished = true IsPublished = true,
}; };
await _.Client.Schemas.PostSchemaAsync(schemaRequest); await _.Client.Schemas.PostSchemaAsync(schemaRequest);
@ -60,21 +60,21 @@ public class ContentCollationTests(CreatedAppFixture fixture) : IClassFixture<Cr
await contents.CreateAsync( await contents.CreateAsync(
new SimpleEntityData new SimpleEntityData
{ {
String = "İstanbul" String = "İstanbul",
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
await contents.CreateAsync( await contents.CreateAsync(
new SimpleEntityData new SimpleEntityData
{ {
String = "Mersin" String = "Mersin",
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
await contents.CreateAsync( await contents.CreateAsync(
new SimpleEntityData new SimpleEntityData
{ {
String = "Lüleburgaz" String = "Lüleburgaz",
}, },
ContentCreateOptions.AsPublish); 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?> Localized = new Dictionary<string, string?>
{ {
["de"] = "Hallo", ["de"] = "Hallo",
["en"] = "Hello" ["en"] = "Hello",
} },
}, },
ContentCreateOptions.AsPublish, ContentCreateOptions.AsPublish,
QueryContext.Default.WithLanguages("de")); QueryContext.Default.WithLanguages("de"));
@ -51,8 +51,8 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
{ {
["de"] = "Hallo", ["de"] = "Hallo",
["en"] = "Hello", ["en"] = "Hello",
["custom"] = "Custom" ["custom"] = "Custom",
} },
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -75,8 +75,8 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
Localized = new Dictionary<string, string?> Localized = new Dictionary<string, string?>
{ {
["de"] = "Hallo", ["de"] = "Hallo",
["en"] = "Hello" ["en"] = "Hello",
} },
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -88,14 +88,14 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
content.Id, content.Id,
new Dictionary<string, string> new Dictionary<string, string>
{ {
["X-Flatten"] = "1" ["X-Flatten"] = "1",
}); });
var (etag3, _) = await GetEtagAsync( var (etag3, _) = await GetEtagAsync(
content.Id, content.Id,
new Dictionary<string, string> new Dictionary<string, string>
{ {
["X-Languages"] = "en" ["X-Languages"] = "en",
}); });
var (etag4, _) = await GetEtagAsync( var (etag4, _) = await GetEtagAsync(
@ -103,7 +103,7 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture<Conten
new Dictionary<string, string> new Dictionary<string, string>
{ {
["X-Languages"] = "en", ["X-Languages"] = "en",
["X-Flatten"] = "1" ["X-Flatten"] = "1",
}); });
static void AssertValue(string? value, string? not = null) 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 nested1 = new
{ {
nested2 = index nested2 = index,
} },
}), }),
Geo = GeoJson.Point( Geo = GeoJson.Point(
index + 100, index + 100,
@ -45,7 +45,7 @@ public sealed class ContentQueryFixture : TestSchemaFixtureBase
Searchable = $"text{index}", Searchable = $"text{index}",
Localized = new Dictionary<string, string?> Localized = new Dictionary<string, string?>
{ {
["en"] = index.ToString(CultureInfo.InvariantCulture) ["en"] = index.ToString(CultureInfo.InvariantCulture),
}, },
String = 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 new
{ {
path = "data.number.iv" path = "data.number.iv",
} },
}, },
filter = new filter = new
{ {
@ -149,10 +149,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
path = "id", path = "id",
op = "eq", op = "eq",
value = x.Id value = x.Id,
}).ToArray() }).ToArray(),
} },
} },
}; };
var items_1 = await _.Contents.GetAsync(q_1); var items_1 = await _.Contents.GetAsync(q_1);
@ -210,10 +210,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
new new
{ {
path = "data.number.iv" path = "data.number.iv",
} },
} },
} },
}; };
var items = await _.Contents.GetAsync(q); var items = await _.Contents.GetAsync(q);
@ -238,8 +238,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
JsonQuery = new JsonQuery = new
{ {
random = 5 random = 5,
} },
}; };
var items = await _.Contents.GetAsync(q); var items = await _.Contents.GetAsync(q);
@ -268,11 +268,11 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
new new
{ {
path = "data.number.iv" path = "data.number.iv",
} },
}, },
skip = 5 skip = 5,
} },
}; };
var items = await _.Contents.GetAsync(q); var items = await _.Contents.GetAsync(q);
@ -302,11 +302,11 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
new new
{ {
path = "data.number.iv" path = "data.number.iv",
} },
}, },
top = 5 top = 5,
} },
}; };
var items = await _.Contents.GetAsync(q); var items = await _.Contents.GetAsync(q);
@ -335,8 +335,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
new new
{ {
path = "data.number.iv" path = "data.number.iv",
} },
}, },
filter = new filter = new
{ {
@ -346,17 +346,17 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
path = "data.number.iv", path = "data.number.iv",
op = "gt", op = "gt",
value = 3 value = 3,
}, },
new new
{ {
path = "data.number.iv", path = "data.number.iv",
op = "lt", op = "lt",
value = 7 value = 7,
} },
} },
} },
} },
}; };
var items = await _.Contents.GetAsync(q); var items = await _.Contents.GetAsync(q);
@ -375,8 +375,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
new new
{ {
path = "data.json.iv.nested1.nested2" path = "data.json.iv.nested1.nested2",
} },
}, },
filter = new filter = new
{ {
@ -386,17 +386,17 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
path = "data.json.iv.nested1.nested2", path = "data.json.iv.nested1.nested2",
op = "gt", op = "gt",
value = 3 value = 3,
}, },
new new
{ {
path = "data.json.iv.nested1.nested2", path = "data.json.iv.nested1.nested2",
op = "lt", op = "lt",
value = 7 value = 7,
} },
} },
} },
} },
}; };
var items = await _.Contents.GetAsync(q); var items = await _.Contents.GetAsync(q);
@ -421,8 +421,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
JsonQuery = new JsonQuery = new
{ {
fullText = "text2" fullText = "text2",
} },
}; };
var items = await _.Contents.PollAsync(q, x => true); var items = await _.Contents.PollAsync(q, x => true);
@ -455,10 +455,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
longitude = 103, longitude = 103,
latitude = 3, latitude = 3,
distance = 1000 distance = 1000,
} },
} },
} },
}; };
var items = await _.Contents.PollAsync(q, x => true); var items = await _.Contents.PollAsync(q, x => true);
@ -488,8 +488,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
Json = new JObject Json = new JObject
{ {
["search.field.with.dot"] = 42 ["search.field.with.dot"] = 42,
} },
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -507,11 +507,11 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
path = "data.json.iv.search\\.field\\.with\\.dot", path = "data.json.iv.search\\.field\\.with\\.dot",
op = "eq", op = "eq",
value = 42 value = 42,
} },
} },
} },
} },
}; };
var queried = await _.Contents.GetAsync(q); var queried = await _.Contents.GetAsync(q);
@ -542,10 +542,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
longitude = 104, longitude = 104,
latitude = 4, latitude = 4,
distance = 1000 distance = 1000,
} },
} },
} },
}; };
var items = await _.Contents.PollAsync(q, x => true); 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); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
@ -604,10 +604,10 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
{ {
number = new number = new
{ {
iv = 998 iv = 998,
} },
} },
} },
}; };
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
@ -635,8 +635,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}", }",
variables = new 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 var query2 = new
@ -654,8 +654,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}", }",
variables = new 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]); var results = await _.Client.SharedDynamicContents.GraphQlAsync<QueryResult>([query1, query2]);
@ -685,8 +685,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}", }",
variables = new 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); var result = await _.Client.SharedDynamicContents.GraphQlAsync<QueryResult>(query);
@ -713,8 +713,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}", }",
variables = new 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); 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); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
@ -765,8 +765,8 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture<Cont
}", }",
variables = new variables = new
{ {
search = @"The answer is 42" search = @"The answer is 42",
} },
}; };
await _.Client.SharedDynamicContents.GraphQlAsync<QueryResult>(query); 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( var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = null References = null,
}); });
@ -32,7 +32,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentB_1 = await _.Contents.CreateAsync( var contentB_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = [contentA_1.Id] References = [contentA_1.Id],
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -48,7 +48,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
contentA_1.Id, contentA_1.Id,
new ChangeStatus new ChangeStatus
{ {
Status = "Published" Status = "Published",
}); });
@ -65,7 +65,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync( var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = null References = null,
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -74,7 +74,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync( await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = [contentA_1.Id] References = [contentA_1.Id],
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -99,7 +99,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync( var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = null References = null,
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -108,7 +108,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync( await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = [contentA_1.Id] References = [contentA_1.Id],
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -122,7 +122,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{ {
Status = "Draft", Status = "Draft",
// Ensure that the flag is true. // Ensure that the flag is true.
CheckReferrers = true CheckReferrers = true,
}); });
}); });
@ -134,7 +134,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{ {
Status = "Draft", Status = "Draft",
// It is the default anyway, just to make it more explicit. // 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( var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = null References = null,
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -154,7 +154,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync( await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = [contentA_1.Id] References = [contentA_1.Id],
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -169,10 +169,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{ {
Id = contentA_1.Id, Id = contentA_1.Id,
Type = BulkUpdateType.Delete, Type = BulkUpdateType.Delete,
Status = "Draft" Status = "Draft",
}, },
], ],
CheckReferrers = true CheckReferrers = true,
}); });
Assert.NotNull(result1[0].Error); Assert.NotNull(result1[0].Error);
@ -188,10 +188,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{ {
Id = contentA_1.Id, Id = contentA_1.Id,
Type = BulkUpdateType.Delete, Type = BulkUpdateType.Delete,
Status = "Draft" Status = "Draft",
}, },
], ],
CheckReferrers = false CheckReferrers = false,
}); });
Assert.Null(result2[0].Error); Assert.Null(result2[0].Error);
@ -204,7 +204,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
var contentA_1 = await _.Contents.CreateAsync( var contentA_1 = await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = null References = null,
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -213,7 +213,7 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
await _.Contents.CreateAsync( await _.Contents.CreateAsync(
new TestEntityWithReferencesData new TestEntityWithReferencesData
{ {
References = [contentA_1.Id] References = [contentA_1.Id],
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -228,10 +228,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{ {
Id = contentA_1.Id, Id = contentA_1.Id,
Type = BulkUpdateType.ChangeStatus, Type = BulkUpdateType.ChangeStatus,
Status = "Draft" Status = "Draft",
}, },
], ],
CheckReferrers = true CheckReferrers = true,
}); });
Assert.NotNull(result1[0].Error); Assert.NotNull(result1[0].Error);
@ -247,10 +247,10 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi
{ {
Id = contentA_1.Id, Id = contentA_1.Id,
Type = BulkUpdateType.ChangeStatus, Type = BulkUpdateType.ChangeStatus,
Status = "Draft" Status = "Draft",
}, },
], ],
CheckReferrers = false CheckReferrers = false,
}); });
Assert.Null(result2[0].Error); Assert.Null(result2[0].Error);

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

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

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

@ -71,10 +71,10 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
Name = "name", Name = "name",
Properties = new StringFieldPropertiesDto() Properties = new StringFieldPropertiesDto(),
}, },
], ],
Type = SchemaType.Component Type = SchemaType.Component,
}; };
var locationsId = await CreateSchemaAsync(createLocationRequest); var locationsId = await CreateSchemaAsync(createLocationRequest);
@ -89,7 +89,7 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
Name = "name", Name = "name",
Properties = new StringFieldPropertiesDto() Properties = new StringFieldPropertiesDto(),
}, },
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
@ -98,9 +98,9 @@ public sealed class GraphQLFixture : ContentFixture
{ {
SchemaIds = SchemaIds =
[ [
locationsId locationsId,
] ],
} },
}, },
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
@ -109,12 +109,12 @@ public sealed class GraphQLFixture : ContentFixture
{ {
SchemaIds = SchemaIds =
[ [
locationsId locationsId,
] ],
} },
}, },
], ],
IsPublished = true IsPublished = true,
}; };
var citiesId = await CreateSchemaAsync(createCitiesRequest); var citiesId = await CreateSchemaAsync(createCitiesRequest);
@ -129,18 +129,18 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
Name = "name", Name = "name",
Properties = new StringFieldPropertiesDto() Properties = new StringFieldPropertiesDto(),
}, },
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
Name = "cities", Name = "cities",
Properties = new ReferencesFieldPropertiesDto Properties = new ReferencesFieldPropertiesDto
{ {
SchemaIds = [citiesId] SchemaIds = [citiesId],
} },
}, },
], ],
IsPublished = true IsPublished = true,
}; };
var statesId = await CreateSchemaAsync(createStatesRequest); var statesId = await CreateSchemaAsync(createStatesRequest);
@ -155,18 +155,18 @@ public sealed class GraphQLFixture : ContentFixture
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
Name = "name", Name = "name",
Properties = new StringFieldPropertiesDto() Properties = new StringFieldPropertiesDto(),
}, },
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
Name = "states", Name = "states",
Properties = new ReferencesFieldPropertiesDto Properties = new ReferencesFieldPropertiesDto
{ {
SchemaIds = [statesId] SchemaIds = [statesId],
} },
}, },
], ],
IsPublished = true IsPublished = true,
}; };
await CreateSchemaAsync(createCountriesRequest); await CreateSchemaAsync(createCountriesRequest);
@ -187,14 +187,14 @@ public sealed class GraphQLFixture : ContentFixture
{ {
name = new name = new
{ {
iv = name iv = name,
}, },
topLocation = new topLocation = new
{ {
iv = new iv = new
{ {
name = $"{name} Top Location" name = $"{name} Top Location",
} },
}, },
locations = new locations = new
{ {
@ -207,9 +207,9 @@ public sealed class GraphQLFixture : ContentFixture
new new
{ {
name = $"{name} Location 2", name = $"{name} Location 2",
} },
} },
} },
}; };
var city = await Cities.CreateAsync(cityData, ContentCreateOptions.AsPublish); var city = await Cities.CreateAsync(cityData, ContentCreateOptions.AsPublish);
@ -223,12 +223,12 @@ public sealed class GraphQLFixture : ContentFixture
{ {
name = new name = new
{ {
iv = name iv = name,
}, },
cities = new cities = new
{ {
iv = new[] { cityId } iv = new[] { cityId },
} },
}; };
var state = await States.CreateAsync(stateData, ContentCreateOptions.AsPublish); var state = await States.CreateAsync(stateData, ContentCreateOptions.AsPublish);
@ -251,12 +251,12 @@ public sealed class GraphQLFixture : ContentFixture
{ {
name = new name = new
{ {
iv = "Germany" iv = "Germany",
}, },
states = new states = new
{ {
iv = new[] { saxonyState, bavariaState } iv = new[] { saxonyState, bavariaState },
} },
}; };
await Countries.CreateAsync(countryData, ContentCreateOptions.AsPublish); 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 { contentChanges {
id id
} }
}" }",
}; };
var contentId = Guid.NewGuid().ToString(); var contentId = Guid.NewGuid().ToString();
@ -95,7 +95,7 @@ public class GraphQLSubscriptionTests(ContentFixture fixture) : IClassFixture<Co
assetChanges { assetChanges {
id id
} }
}" }",
}; };
var assetId = Guid.NewGuid().ToString(); var assetId = Guid.NewGuid().ToString();
@ -128,7 +128,7 @@ public class GraphQLSubscriptionTests(ContentFixture fixture) : IClassFixture<Co
var options = new GraphQLHttpClientOptions 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()); 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 variables = new
{ {
id = asset.Id, id = asset.Id,
} },
}; };
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -100,9 +100,9 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
value = 1, value = 1,
obj = new obj = new
{ {
value = 2 value = 2,
} },
}) }),
}, },
ContentCreateOptions.AsPublish); ContentCreateOptions.AsPublish);
@ -121,7 +121,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
variables = new variables = new
{ {
id = content_0.Id, id = content_0.Id,
} },
}; };
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); 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 result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cities = result?["cities"]?.ToObject<List<City>>(); var cities = result?["cities"]?.ToObject<List<City>>();
cities.Should().BeEquivalentTo(new List<City> cities.Should().BeEquivalentTo(
{ [
new City new City
{ {
Data = new CityData Data = new CityData
@ -164,20 +164,20 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
Name = "Leipzig", Name = "Leipzig",
TopLocation = new LocationData TopLocation = new LocationData
{ {
Name = "Leipzig Top Location" Name = "Leipzig Top Location",
}, },
Locations = Locations =
[ [
new LocationData new LocationData
{ {
Name = "Leipzig Location 1" Name = "Leipzig Location 1",
}, },
new LocationData new LocationData
{ {
Name = "Leipzig Location 2" Name = "Leipzig Location 2",
}, },
], ],
} },
}, },
new City new City
{ {
@ -186,22 +186,22 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
Name = "Munich", Name = "Munich",
TopLocation = new LocationData TopLocation = new LocationData
{ {
Name = "Munich Top Location" Name = "Munich Top Location",
}, },
Locations = Locations =
[ [
new LocationData new LocationData
{ {
Name = "Munich Location 1" Name = "Munich Location 1",
}, },
new LocationData new LocationData
{ {
Name = "Munich Location 2" Name = "Munich Location 2",
}, },
], ],
} },
} },
}); ]);
} }
[Fact] [Fact]
@ -226,7 +226,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
} }
} }
} }
}" }",
}; };
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); 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); 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); 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); 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); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -380,7 +380,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
cities: queryCitiesContents { cities: queryCitiesContents {
data__dynamic data__dynamic
} }
}" }",
}; };
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -403,7 +403,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
queryCitiesContents { queryCitiesContents {
id id
} }
}" }",
}; };
var url = _.Client.GenerateUrl($"api/content/{_.AppName}/graphql/batch"); var url = _.Client.GenerateUrl($"api/content/{_.AppName}/graphql/batch");
@ -445,8 +445,8 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
}", }",
variables = new variables = new
{ {
ids ids,
} },
}; };
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); 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, id1 = allCities.Items[0].Id,
id2 = allCities.Items[1].Id, id2 = allCities.Items[1].Id,
} },
}; };
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
@ -507,7 +507,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
queryCitiesContents { queryCitiesContents {
id id
} }
}" }",
}; };
// Create the request manually to check the headers. // Create the request manually to check the headers.
@ -526,7 +526,7 @@ public sealed class GraphQLTests(GraphQLFixture fixture) : IClassFixture<GraphQL
"X-NoResolveLanguages", "X-NoResolveLanguages",
"X-ResolveFlow", "X-ResolveFlow",
"X-ResolveUrls", "X-ResolveUrls",
"X-Unpublished" "X-Unpublished",
}, httpResponse.Headers.Vary.Order().ToArray()); }, 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, Method = WebhookMethod.POST,
Payload = null, Payload = null,
PayloadType = null, PayloadType = null,
Url = new Uri("http://squidex.io") Url = "http://squidex.io",
} },
} },
} },
}, },
Trigger = new ManualRuleTriggerDto(), Trigger = new ManualRuleTriggerDto(),
}; };

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

@ -50,17 +50,17 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
Step = new WebhookFlowStepDto Step = new WebhookFlowStepDto
{ {
Url = new Uri(url), Url = url,
// Also test the secret in this case. // Also test the secret in this case.
SharedSecret = secret SharedSecret = secret,
} },
} },
} },
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
HandleAll = true HandleAll = true,
} },
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -103,7 +103,7 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
var referencedContent = await referencedContents.CreateAsync( var referencedContent = await referencedContents.CreateAsync(
new TestEntityData new TestEntityData
{ {
String = contentString String = contentString,
}); });
var parentSchema = await TestEntityWithReferences.CreateSchemaAsync(app.Schemas, schemaNameRef); var parentSchema = await TestEntityWithReferences.CreateSchemaAsync(app.Schemas, schemaNameRef);
@ -116,7 +116,7 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
References = References =
[ [
referencedContent.Id referencedContent.Id,
], ],
}); });
@ -142,12 +142,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
complete(payload); complete(payload);
}}); }});
)", )",
Url = new Uri(url), Url = url,
// Also test the secret in this case. // Also test the secret in this case.
SharedSecret = secret SharedSecret = secret,
} },
} },
} },
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
@ -155,17 +155,17 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
[ [
new SchemaCondition new SchemaCondition
{ {
SchemaId = parentSchema.Id SchemaId = parentSchema.Id,
}, },
], ],
ReferencedSchemas = ReferencedSchemas =
[ [
new SchemaCondition new SchemaCondition
{ {
SchemaId = referencedSchema.Id SchemaId = referencedSchema.Id,
}, },
] ],
} },
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -179,7 +179,7 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
referencedContent.Id, referencedContent.Id,
new TestEntityData new TestEntityData
{ {
String = updatedString String = updatedString,
}); });
@ -222,15 +222,15 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
Script = $@" Script = $@"
postJSON('{url}', {{ schemaName: event.schemaId.Name }}, function () {{}}) postJSON('{url}', {{ schemaName: event.schemaId.Name }}, function () {{}})
" ",
} },
} },
} },
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
HandleAll = true HandleAll = true,
} },
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -276,14 +276,14 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
Step = new WebhookFlowStepDto Step = new WebhookFlowStepDto
{ {
Url = new Uri(url), Url = url,
// Also test the secret in this case. // Also test the secret in this case.
SharedSecret = secret SharedSecret = secret,
} },
} },
} },
}, },
Trigger = new AssetChangedRuleTriggerDto() Trigger = new AssetChangedRuleTriggerDto(),
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -335,12 +335,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
updateAsset(event, metadata); updateAsset(event, metadata);
}); });
" ",
} },
} },
} },
}, },
Trigger = new AssetChangedRuleTriggerDto() Trigger = new AssetChangedRuleTriggerDto(),
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -378,14 +378,14 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
Step = new WebhookFlowStepDto Step = new WebhookFlowStepDto
{ {
Url = new Uri(url), Url = url,
// Also test the secret in this case. // Also test the secret in this case.
SharedSecret = secret SharedSecret = secret,
} },
} },
} },
}, },
Trigger = new SchemaChangedRuleTriggerDto() Trigger = new SchemaChangedRuleTriggerDto(),
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -431,14 +431,14 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
Step = new WebhookFlowStepDto Step = new WebhookFlowStepDto
{ {
Url = new Uri(url), Url = url,
// Also test the secret in this case. // Also test the secret in this case.
SharedSecret = secret SharedSecret = secret,
} },
} },
} },
}, },
Trigger = new ManualRuleTriggerDto() Trigger = new ManualRuleTriggerDto(),
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -486,17 +486,17 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
Step = new WebhookFlowStepDto Step = new WebhookFlowStepDto
{ {
Url = new Uri(url), Url = url,
// Also test the secret in this case. // Also test the secret in this case.
SharedSecret = secret SharedSecret = secret,
} },
} },
} },
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
HandleAll = true HandleAll = true,
} },
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -544,12 +544,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
console.log('Hello Log'); console.log('Hello Log');
console.warn('Hello warn'); console.warn('Hello warn');
" ",
} },
} },
} },
}, },
Trigger = new ManualRuleTriggerDto() Trigger = new ManualRuleTriggerDto(),
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -596,8 +596,8 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
Condition = "event.value.testValue == 2", Condition = "event.value.testValue == 2",
NextStepId = stepId3, NextStepId = stepId3,
}, },
] ],
} },
}, },
[stepId2.ToString()] = new FlowStepDefinitionDto [stepId2.ToString()] = new FlowStepDefinitionDto
{ {
@ -606,8 +606,8 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
Script = @" Script = @"
console.info('Hello from Branch1'); console.info('Hello from Branch1');
" ",
} },
}, },
[stepId3.ToString()] = new FlowStepDefinitionDto [stepId3.ToString()] = new FlowStepDefinitionDto
{ {
@ -616,12 +616,12 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
Script = @" Script = @"
console.info('Hello from Branch2'); console.info('Hello from Branch2');
" ",
} },
} },
} },
}, },
Trigger = new ManualRuleTriggerDto() Trigger = new ManualRuleTriggerDto(),
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -632,8 +632,8 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo
{ {
Value = new Dictionary<string, int> Value = new Dictionary<string, int>
{ {
["testValue"] = 1 ["testValue"] = 1,
} },
}); });
var @event1 = await app.Rules.PollEventAsync(rule.Id, x => x.FlowState.Status == FlowExecutionStatus.Completed); 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> 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); 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( await contents.CreateAsync(
new TestEntityData 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, Method = WebhookMethod.POST,
Payload = null, Payload = null,
PayloadType = null, PayloadType = null,
Url = new Uri("http://squidex.io") Url = "http://squidex.io",
} },
} },
} },
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
HandleAll = true HandleAll = true,
} },
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -76,12 +76,12 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
Method = WebhookMethod.POST, Method = WebhookMethod.POST,
Payload = null, Payload = null,
PayloadType = null, PayloadType = null,
Url = new Uri("http://squidex.io") Url = new Uri("http://squidex.io"),
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
HandleAll = true HandleAll = true,
} },
}; };
var rule = await app.Rules.PostRuleAsync(createRule); var rule = await app.Rules.PostRuleAsync(createRule);
@ -112,15 +112,15 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
Method = WebhookMethod.POST, Method = WebhookMethod.POST,
Payload = null, Payload = null,
PayloadType = null, PayloadType = null,
Url = new Uri("http://squidex.io") Url = "http://squidex.io",
} },
} },
} },
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
HandleAll = true HandleAll = true,
} },
}; };
var rule_0 = await app.Rules.PostRuleAsync(createRequest); var rule_0 = await app.Rules.PostRuleAsync(createRequest);
@ -129,7 +129,7 @@ public class RuleTests(ClientFixture fixture) : IClassFixture<ClientFixture>
// STEP 2: Update rule. // STEP 2: Update rule.
var updateRequest = new UpdateRuleDto var updateRequest = new UpdateRuleDto
{ {
Name = ruleName Name = ruleName,
}; };
var rule_1 = await app.Rules.PutRuleAsync(rule_0.Id, updateRequest); 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, Method = WebhookMethod.POST,
Payload = null, Payload = null,
PayloadType = null, PayloadType = null,
Url = new Uri("http://squidex.io") Url = "http://squidex.io",
} },
} },
} },
}, },
Trigger = new ContentChangedRuleTriggerDto Trigger = new ContentChangedRuleTriggerDto
{ {
HandleAll = true HandleAll = true,
} },
}; };
var rule = await app.Rules.PostRuleAsync(createRequest); 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] [Fact]
public async Task Should_validate_valid_step() 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); 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. // STEP 1: Create schema.
var createRequest = new CreateSchemaDto var createRequest = new CreateSchemaDto
{ {
Name = schemaName Name = schemaName,
}; };
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest); var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -48,7 +48,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// STEP 1: Create schema. // STEP 1: Create schema.
var createRequest = new CreateSchemaDto var createRequest = new CreateSchemaDto
{ {
Name = schemaName Name = schemaName,
}; };
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest); 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. // Use the new property to create a singleton.
Type = SchemaType.Singleton, Type = SchemaType.Singleton,
// Must be pusblished to query content. // Must be pusblished to query content.
IsPublished = true IsPublished = true,
}; };
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest); 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. // Use the old property to create a singleton.
IsSingleton = true, IsSingleton = true,
// Must be pusblished to query content. // Must be pusblished to query content.
IsPublished = true IsPublished = true,
}; };
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest); var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -148,8 +148,8 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
Partitioning = "invariant", Partitioning = "invariant",
Properties = new ReferencesFieldPropertiesDto Properties = new ReferencesFieldPropertiesDto
{ {
Editor = ReferencesFieldEditor.Checkboxes Editor = ReferencesFieldEditor.Checkboxes,
} },
}, },
new UpsertSchemaFieldDto new UpsertSchemaFieldDto
{ {
@ -158,10 +158,10 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
Properties = new TagsFieldPropertiesDto Properties = new TagsFieldPropertiesDto
{ {
Editor = TagsFieldEditor.Checkboxes, Editor = TagsFieldEditor.Checkboxes,
AllowedValues = ["value1"] AllowedValues = ["value1"],
} },
}, },
] ],
}; };
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest); var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -176,7 +176,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// STEP 1: Create schema. // STEP 1: Create schema.
var createRequest = new CreateSchemaDto var createRequest = new CreateSchemaDto
{ {
Name = schemaName Name = schemaName,
}; };
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest); var schema = await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -200,7 +200,7 @@ public class SchemaTests(CreatedAppFixture fixture) : IClassFixture<CreatedAppFi
// STEP 1: Create schema. // STEP 1: Create schema.
var createRequest = new CreateSchemaDto var createRequest = new CreateSchemaDto
{ {
Name = schemaName Name = schemaName,
}; };
var schema = await _.Client.Schemas.PostSchemaAsync(createRequest); 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 var createRequest = new CreateSchemaDto
{ {
Name = schemaName Name = schemaName,
}; };
await _.Client.Schemas.PostSchemaAsync(createRequest); await _.Client.Schemas.PostSchemaAsync(createRequest);
@ -59,7 +59,7 @@ public class SearchTests(ContentFixture fixture) : IClassFixture<ContentFixture>
var createRequest = new TestEntityData var createRequest = new TestEntityData
{ {
String = contentString String = contentString,
}; };
await _.Contents.CreateAsync(createRequest, ContentCreateOptions.AsPublish); 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. // STEP 1: Do not invite contributors when flag is false.
var createRequest = new AssignContributorDto var createRequest = new AssignContributorDto
{ {
ContributorId = "test@squidex.io" ContributorId = "test@squidex.io",
}; };
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => 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 must be true, otherwise new users are not created.
Invite = true, Invite = true,
// This is the only allowed role for teams. // This is the only allowed role for teams.
Role = "Owner" Role = "Owner",
}; };
var contributors = await _.Client.Teams.PostContributorAsync(teamId, createInviteRequest); 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. // STEP 1: Update app.
var updateRequest = new UpdateTeamDto var updateRequest = new UpdateTeamDto
{ {
Name = Guid.NewGuid().ToString() Name = Guid.NewGuid().ToString(),
}; };
var app_1 = await _.Client.Teams.PutTeamAsync(_.TeamId, updateRequest); 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. // STEP 1: Assign app to team.
var transferRequest = new TransferToTeamDto var transferRequest = new TransferToTeamDto
{ {
TeamId = _.TeamId TeamId = _.TeamId,
}; };
var app_1 = await client.Apps.PutAppTeamAsync(transferRequest); 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. // STEP 1: Assign app to team.
var transferRequest = new TransferToTeamDto var transferRequest = new TransferToTeamDto
{ {
TeamId = _.TeamId TeamId = _.TeamId,
}; };
var app_1 = await client.Apps.PutAppTeamAsync(transferRequest); 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. // STEP 2: Remove app from team.
var untransferRequest = new TransferToTeamDto var untransferRequest = new TransferToTeamDto
{ {
TeamId = null TeamId = null,
}; };
var app_2 = await client.Apps.PutAppTeamAsync(untransferRequest); var app_2 = await client.Apps.PutAppTeamAsync(untransferRequest);
@ -94,7 +94,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 1: Assign scheme. // STEP 1: Assign scheme.
var request = new AuthSchemeValueDto var request = new AuthSchemeValueDto
{ {
Scheme = scheme Scheme = scheme,
}; };
var scheme_0 = await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request); var scheme_0 = await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request);
@ -123,7 +123,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 0: Assign scheme. // STEP 0: Assign scheme.
var request1 = new AuthSchemeValueDto var request1 = new AuthSchemeValueDto
{ {
Scheme = scheme Scheme = scheme,
}; };
await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request1); await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request1);
@ -132,7 +132,7 @@ public class TeamTests(CreatedTeamFixture fixture) : IClassFixture<CreatedTeamFi
// STEP 1: Unassign scheme. // STEP 1: Unassign scheme.
var request2 = new AuthSchemeValueDto var request2 = new AuthSchemeValueDto
{ {
Scheme = null Scheme = null,
}; };
var scheme_0 = await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request2); var scheme_0 = await _.Client.Teams.PutTeamAuthAsync(_.TeamId, request2);

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

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

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

Loading…
Cancel
Save