Browse Source

Feature/components (#698)

* Started to work on components.

* Cleanup

* Just some stupid refactoring.

* Continued with components.

* Backend structure for component.

* UI fixes.

* Better handling for unpublished schemas.

* Saving changes.

* Fix OpenAPI

* Tests fixed.

* Support for components.

* Recursive level guard.

* Schema fix.
pull/699/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
6dc7ca4d38
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      backend/i18n/frontend_en.json
  2. 8
      backend/i18n/frontend_it.json
  3. 8
      backend/i18n/frontend_nl.json
  4. 4
      backend/i18n/source/backend_en.json
  5. 8
      backend/i18n/source/frontend_en.json
  6. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs
  7. 6
      backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs
  8. 57
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentFieldProperties.cs
  9. 61
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs
  10. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldBase.cs
  11. 44
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
  12. 168
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
  13. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs
  14. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
  15. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
  16. 23
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IMetadataProvider.cs
  17. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaType.cs
  18. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs
  19. 165
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  20. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs
  21. 52
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs
  22. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs
  23. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  24. 60
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs
  25. 69
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs
  26. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs
  27. 141
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  28. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  29. 47
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  30. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs
  31. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs
  32. 79
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  33. 89
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs
  34. 37
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs
  35. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs
  36. 70
      backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
  37. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs
  38. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  39. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs
  40. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs
  41. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs
  42. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs
  43. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs
  44. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  45. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs
  46. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs
  47. 17
      backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/FieldPropertiesValidator.cs
  48. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs
  49. 1
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs
  50. 40
      backend/src/Squidex.Shared/Permissions.cs
  51. 12
      backend/src/Squidex.Shared/Texts.it.resx
  52. 12
      backend/src/Squidex.Shared/Texts.nl.resx
  53. 12
      backend/src/Squidex.Shared/Texts.resx
  54. 18
      backend/src/Squidex.Web/ApiController.cs
  55. 4
      backend/src/Squidex.Web/ApiPermissionAttribute.cs
  56. 5
      backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
  57. 4
      backend/src/Squidex.Web/IAppFeature.cs
  58. 4
      backend/src/Squidex.Web/ISchemaFeature.cs
  59. 14
      backend/src/Squidex.Web/Pipeline/AppFeature.cs
  60. 2
      backend/src/Squidex.Web/Pipeline/AppResolver.cs
  61. 14
      backend/src/Squidex.Web/Pipeline/SchemaFeature.cs
  62. 13
      backend/src/Squidex.Web/Pipeline/SchemaResolver.cs
  63. 8
      backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs
  64. 2
      backend/src/Squidex.Web/Resources.cs
  65. 16
      backend/src/Squidex.Web/SchemaMustBePublishedAttribute.cs
  66. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  67. 8
      backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs
  68. 121
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  69. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs
  70. 35
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs
  71. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs
  72. 10
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  73. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  74. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs
  75. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs
  76. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs
  77. 10
      backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  78. 10
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs
  79. 4
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs
  80. 29
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs
  81. 39
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs
  82. 2
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs
  83. 2
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
  84. 118
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  85. 92
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  86. 12
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs
  87. 124
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs
  88. 48
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs
  89. 8
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs
  90. 87
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs
  91. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs
  92. 137
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs
  93. 187
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs
  94. 68
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs
  95. 65
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs
  96. 15
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs
  97. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs
  98. 44
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs
  99. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  100. 44
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ComponentsFieldPropertiesTests.cs

12
backend/i18n/frontend_en.json

@ -231,6 +231,7 @@
"common.cluster": "Cluster", "common.cluster": "Cluster",
"common.clusterPageTitle": "Cluster", "common.clusterPageTitle": "Cluster",
"common.comments": "Comments", "common.comments": "Comments",
"common.components": "Components",
"common.confirm": "Confirm", "common.confirm": "Confirm",
"common.consumers": "Consumers", "common.consumers": "Consumers",
"common.content": "Content", "common.content": "Content",
@ -363,6 +364,7 @@
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflows", "common.workflows": "Workflows",
"common.yes": "Yes", "common.yes": "Yes",
"contents.addComponent": "Add Component",
"contents.arrayAddItem": "Add Item", "contents.arrayAddItem": "Add Item",
"contents.arrayClear": "Clear", "contents.arrayClear": "Clear",
"contents.arrayClearConfirmText": "Do you really want to clear the array?", "contents.arrayClearConfirmText": "Do you really want to clear the array?",
@ -383,6 +385,8 @@
"contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).", "contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).",
"contents.contentTab.editor": "Editor", "contents.contentTab.editor": "Editor",
"contents.contentTab.references": "References", "contents.contentTab.references": "References",
@ -399,8 +403,8 @@
"contents.deleteConfirmText": "Do you really want to delete the content?", "contents.deleteConfirmText": "Do you really want to delete the content?",
"contents.deleteConfirmTitle": "Delete content", "contents.deleteConfirmTitle": "Delete content",
"contents.deleteManyConfirmText": "Do you really want to delete the selected content items?", "contents.deleteManyConfirmText": "Do you really want to delete the selected content items?",
"contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete/unpublish this content?", "contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete this content?",
"contents.deleteReferrerConfirmTitle": "Delete/unpublish content", "contents.deleteReferrerConfirmTitle": "Delete content",
"contents.deleteVersionConfirmText": "Do you really want to delete this version?", "contents.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Failed to delete version. Please reload.", "contents.deleteVersionFailed": "Failed to delete version. Please reload.",
"contents.draftNew": "New Draft", "contents.draftNew": "New Draft",
@ -760,6 +764,8 @@
"schemas.fieldTypes.assets.sizeMax": "Min Size", "schemas.fieldTypes.assets.sizeMax": "Min Size",
"schemas.fieldTypes.assets.sizeMin": "Max Size", "schemas.fieldTypes.assets.sizeMin": "Max Size",
"schemas.fieldTypes.boolean.description": "Yes or no, true or false.", "schemas.fieldTypes.boolean.description": "Yes or no, true or false.",
"schemas.fieldTypes.component.description": "Embed another schema to this content.",
"schemas.fieldTypes.components.description": "Embed another schemas to this content as array.",
"schemas.fieldTypes.dateTime.defaultMode": "Default Mode", "schemas.fieldTypes.dateTime.defaultMode": "Default Mode",
"schemas.fieldTypes.dateTime.description": "Events date, opening hours.", "schemas.fieldTypes.dateTime.description": "Events date, opening hours.",
"schemas.fieldTypes.dateTime.rangeMax": "Max Value", "schemas.fieldTypes.dateTime.rangeMax": "Max Value",
@ -805,6 +811,8 @@
"schemas.loadFailed": "Failed to load schemas. Please reload.", "schemas.loadFailed": "Failed to load schemas. Please reload.",
"schemas.loadSchemaFailed": "Failed to load schema. Please reload.", "schemas.loadSchemaFailed": "Failed to load schema. Please reload.",
"schemas.lockFieldFailed": "Failed to lock field. Please reload.", "schemas.lockFieldFailed": "Failed to lock field. Please reload.",
"schemas.modeComponent": "Component",
"schemas.modeComponentDescription": "Can only be embedded to component fields...",
"schemas.modeMultiple": "Multiple contents", "schemas.modeMultiple": "Multiple contents",
"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",

8
backend/i18n/frontend_it.json

@ -231,6 +231,7 @@
"common.cluster": "Cluster", "common.cluster": "Cluster",
"common.clusterPageTitle": "Cluster", "common.clusterPageTitle": "Cluster",
"common.comments": "Commenti", "common.comments": "Commenti",
"common.components": "Components",
"common.confirm": "Conferma", "common.confirm": "Conferma",
"common.consumers": "Servizi", "common.consumers": "Servizi",
"common.content": "Contenuto", "common.content": "Contenuto",
@ -363,6 +364,7 @@
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflow", "common.workflows": "Workflow",
"common.yes": "Sì", "common.yes": "Sì",
"contents.addComponent": "Add Component",
"contents.arrayAddItem": "Aggiungi un elemento", "contents.arrayAddItem": "Aggiungi un elemento",
"contents.arrayClear": "Pulisci", "contents.arrayClear": "Pulisci",
"contents.arrayClearConfirmText": "Sei sicuro di voler cancellare l'array?", "contents.arrayClearConfirmText": "Sei sicuro di voler cancellare l'array?",
@ -383,6 +385,8 @@
"contents.changeStatusTo": "Cambia gli elementi del contenuto in {action}", "contents.changeStatusTo": "Cambia gli elementi del contenuto in {action}",
"contents.changeStatusToImmediately": "Imposta {action} immediatamente.", "contents.changeStatusToImmediately": "Imposta {action} immediatamente.",
"contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.", "contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Un elemento del contenuto non è valido, verifica il campo con la barra rossa per tutte le lingue impostate (se presenti).", "contents.contentNotValid": "Un elemento del contenuto non è valido, verifica il campo con la barra rossa per tutte le lingue impostate (se presenti).",
"contents.contentTab.editor": "Editor", "contents.contentTab.editor": "Editor",
"contents.contentTab.references": "Collegato a", "contents.contentTab.references": "Collegato a",
@ -760,6 +764,8 @@
"schemas.fieldTypes.assets.sizeMax": "Dimensione Min", "schemas.fieldTypes.assets.sizeMax": "Dimensione Min",
"schemas.fieldTypes.assets.sizeMin": "Dimensione Max", "schemas.fieldTypes.assets.sizeMin": "Dimensione Max",
"schemas.fieldTypes.boolean.description": "Si o no, vero o falso.", "schemas.fieldTypes.boolean.description": "Si o no, vero o falso.",
"schemas.fieldTypes.component.description": "Embed another schema to this content.",
"schemas.fieldTypes.components.description": "Embed another schemas to this content as array.",
"schemas.fieldTypes.dateTime.defaultMode": "Modalità predefinita", "schemas.fieldTypes.dateTime.defaultMode": "Modalità predefinita",
"schemas.fieldTypes.dateTime.description": "Data degli eventi, orari di apertura.", "schemas.fieldTypes.dateTime.description": "Data degli eventi, orari di apertura.",
"schemas.fieldTypes.dateTime.rangeMax": "Valore Max", "schemas.fieldTypes.dateTime.rangeMax": "Valore Max",
@ -805,6 +811,8 @@
"schemas.loadFailed": "Non è stato possibile caricare gli schemi. Per favore ricarica.", "schemas.loadFailed": "Non è stato possibile caricare gli schemi. Per favore ricarica.",
"schemas.loadSchemaFailed": "Non è stato possibile caricare lo schema. Per favore ricarica.", "schemas.loadSchemaFailed": "Non è stato possibile caricare lo schema. Per favore ricarica.",
"schemas.lockFieldFailed": "Non è stato possibile bloccare il campo. Per favore ricarica.", "schemas.lockFieldFailed": "Non è stato possibile bloccare il campo. Per favore ricarica.",
"schemas.modeComponent": "Component",
"schemas.modeComponentDescription": "Can only be embedded to component fields...",
"schemas.modeMultiple": "Contenuti multipli", "schemas.modeMultiple": "Contenuti multipli",
"schemas.modeMultipleDescription": "Ideale per contenuti multipli come post di blog, pagine, autori, prodotti...", "schemas.modeMultipleDescription": "Ideale per contenuti multipli come post di blog, pagine, autori, prodotti...",
"schemas.modeSingle": "Singolo contenuto", "schemas.modeSingle": "Singolo contenuto",

8
backend/i18n/frontend_nl.json

@ -231,6 +231,7 @@
"common.cluster": "Cluster", "common.cluster": "Cluster",
"common.clusterPageTitle": "Cluster", "common.clusterPageTitle": "Cluster",
"common.comments": "Reacties", "common.comments": "Reacties",
"common.components": "Components",
"common.confirm": "Bevestigen", "common.confirm": "Bevestigen",
"common.consumers": "Consumenten", "common.consumers": "Consumenten",
"common.content": "Inhoud", "common.content": "Inhoud",
@ -363,6 +364,7 @@
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflows", "common.workflows": "Workflows",
"common.yes": "Ja", "common.yes": "Ja",
"contents.addComponent": "Add Component",
"contents.arrayAddItem": "Item toevoegen", "contents.arrayAddItem": "Item toevoegen",
"contents.arrayClear": "Clear", "contents.arrayClear": "Clear",
"contents.arrayClearConfirmText": "Do you really want to clear the array?", "contents.arrayClearConfirmText": "Do you really want to clear the array?",
@ -383,6 +385,8 @@
"contents.changeStatusTo": "Verander inhoud item(s) in {action}", "contents.changeStatusTo": "Verander inhoud item(s) in {action}",
"contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.", "contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.",
"contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.", "contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Inhoudselement niet geldig, controleer het veld met de rode balk aan de linkerkant in alle talen (indien lokaliseerbaar).", "contents.contentNotValid": "Inhoudselement niet geldig, controleer het veld met de rode balk aan de linkerkant in alle talen (indien lokaliseerbaar).",
"contents.contentTab.editor": "Editor", "contents.contentTab.editor": "Editor",
"contents.contentTab.references": "References", "contents.contentTab.references": "References",
@ -760,6 +764,8 @@
"schemas.fieldTypes.assets.sizeMax": "Min. grootte", "schemas.fieldTypes.assets.sizeMax": "Min. grootte",
"schemas.fieldTypes.assets.sizeMin": "Max. grootte", "schemas.fieldTypes.assets.sizeMin": "Max. grootte",
"schemas.fieldTypes.boolean.description": "Ja of nee, waar of niet waar.", "schemas.fieldTypes.boolean.description": "Ja of nee, waar of niet waar.",
"schemas.fieldTypes.component.description": "Embed another schema to this content.",
"schemas.fieldTypes.components.description": "Embed another schemas to this content as array.",
"schemas.fieldTypes.dateTime.defaultMode": "Standaardmodus", "schemas.fieldTypes.dateTime.defaultMode": "Standaardmodus",
"schemas.fieldTypes.dateTime.description": "Datum van evenementen, openingstijden.", "schemas.fieldTypes.dateTime.description": "Datum van evenementen, openingstijden.",
"schemas.fieldTypes.dateTime.rangeMax": "Max. waarde", "schemas.fieldTypes.dateTime.rangeMax": "Max. waarde",
@ -805,6 +811,8 @@
"schemas.loadFailed": "Kan schema's niet laden. Laad opnieuw.", "schemas.loadFailed": "Kan schema's niet laden. Laad opnieuw.",
"schemas.loadSchemaFailed": "Kan schema niet laden. Laad opnieuw.", "schemas.loadSchemaFailed": "Kan schema niet laden. Laad opnieuw.",
"schemas.lockFieldFailed": "Kan veld niet vergrendelen. Laad opnieuw.", "schemas.lockFieldFailed": "Kan veld niet vergrendelen. Laad opnieuw.",
"schemas.modeComponent": "Component",
"schemas.modeComponentDescription": "Can only be embedded to component fields...",
"schemas.modeMultiple": "Meerdere inhoud", "schemas.modeMultiple": "Meerdere inhoud",
"schemas.modeMultipleDescription": "Beste voor meerdere instanties, zoals blogposts, pagina's, auteurs, producten ...", "schemas.modeMultipleDescription": "Beste voor meerdere instanties, zoals blogposts, pagina's, auteurs, producten ...",
"schemas.modeSingle": "Enkele inhoud", "schemas.modeSingle": "Enkele inhoud",

4
backend/i18n/source/backend_en.json

@ -125,6 +125,9 @@
"contents.invalidArrayOfObjects": "Invalid json type, expected array of objects.", "contents.invalidArrayOfObjects": "Invalid json type, expected array of objects.",
"contents.invalidArrayOfStrings": "Invalid json type, expected array of strings.", "contents.invalidArrayOfStrings": "Invalid json type, expected array of strings.",
"contents.invalidBoolean": "Invalid json type, expected boolean.", "contents.invalidBoolean": "Invalid json type, expected boolean.",
"contents.invalidComponentNoObject": "Invalid json object, expected object with 'schemaId' field.",
"contents.invalidComponentNoType": "Invalid component. No 'schemaId' field found.",
"contents.invalidComponentUnknownSchema": "Invalid component. Cannot find schema.",
"contents.invalidGeolocation": "Invalid json type, expected latitude/longitude object.", "contents.invalidGeolocation": "Invalid json type, expected latitude/longitude object.",
"contents.invalidGeolocationLatitude": "Latitude must be between -90 and 90.", "contents.invalidGeolocationLatitude": "Latitude must be between -90 and 90.",
"contents.invalidGeolocationLongitude": "Longitude must be between -180 and 180.", "contents.invalidGeolocationLongitude": "Longitude must be between -180 and 180.",
@ -132,6 +135,7 @@
"contents.invalidString": "Invalid json type, expected string.", "contents.invalidString": "Invalid json type, expected string.",
"contents.listReferences": "{count} Reference(s)", "contents.listReferences": "{count} Reference(s)",
"contents.referenced": "Content is referenced by another content and cannot be deleted or unpublished.", "contents.referenced": "Content is referenced by another content and cannot be deleted or unpublished.",
"contents.schemaNotPublished": "Schema not published.",
"contents.singletonNotChangeable": "Singleton content cannot be updated.", "contents.singletonNotChangeable": "Singleton content cannot be updated.",
"contents.singletonNotCreatable": "Singleton content cannot be created.", "contents.singletonNotCreatable": "Singleton content cannot be created.",
"contents.singletonNotDeletable": "Singleton content cannot be deleted.", "contents.singletonNotDeletable": "Singleton content cannot be deleted.",

8
backend/i18n/source/frontend_en.json

@ -231,6 +231,7 @@
"common.cluster": "Cluster", "common.cluster": "Cluster",
"common.clusterPageTitle": "Cluster", "common.clusterPageTitle": "Cluster",
"common.comments": "Comments", "common.comments": "Comments",
"common.components": "Components",
"common.confirm": "Confirm", "common.confirm": "Confirm",
"common.consumers": "Consumers", "common.consumers": "Consumers",
"common.content": "Content", "common.content": "Content",
@ -363,6 +364,7 @@
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflows", "common.workflows": "Workflows",
"common.yes": "Yes", "common.yes": "Yes",
"contents.addComponent": "Add Component",
"contents.arrayAddItem": "Add Item", "contents.arrayAddItem": "Add Item",
"contents.arrayClear": "Clear", "contents.arrayClear": "Clear",
"contents.arrayClearConfirmText": "Do you really want to clear the array?", "contents.arrayClearConfirmText": "Do you really want to clear the array?",
@ -383,6 +385,8 @@
"contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).", "contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).",
"contents.contentTab.editor": "Editor", "contents.contentTab.editor": "Editor",
"contents.contentTab.references": "References", "contents.contentTab.references": "References",
@ -760,6 +764,8 @@
"schemas.fieldTypes.assets.sizeMax": "Min Size", "schemas.fieldTypes.assets.sizeMax": "Min Size",
"schemas.fieldTypes.assets.sizeMin": "Max Size", "schemas.fieldTypes.assets.sizeMin": "Max Size",
"schemas.fieldTypes.boolean.description": "Yes or no, true or false.", "schemas.fieldTypes.boolean.description": "Yes or no, true or false.",
"schemas.fieldTypes.component.description": "Embed another schema to this content.",
"schemas.fieldTypes.components.description": "Embed another schemas to this content as array.",
"schemas.fieldTypes.dateTime.defaultMode": "Default Mode", "schemas.fieldTypes.dateTime.defaultMode": "Default Mode",
"schemas.fieldTypes.dateTime.description": "Events date, opening hours.", "schemas.fieldTypes.dateTime.description": "Events date, opening hours.",
"schemas.fieldTypes.dateTime.rangeMax": "Max Value", "schemas.fieldTypes.dateTime.rangeMax": "Max Value",
@ -805,6 +811,8 @@
"schemas.loadFailed": "Failed to load schemas. Please reload.", "schemas.loadFailed": "Failed to load schemas. Please reload.",
"schemas.loadSchemaFailed": "Failed to load schema. Please reload.", "schemas.loadSchemaFailed": "Failed to load schema. Please reload.",
"schemas.lockFieldFailed": "Failed to lock field. Please reload.", "schemas.lockFieldFailed": "Failed to lock field. Please reload.",
"schemas.modeComponent": "Component",
"schemas.modeComponentDescription": "Can only be embedded to component fields...",
"schemas.modeMultiple": "Multiple contents", "schemas.modeMultiple": "Multiple contents",
"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",

19
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json.Objects;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed record Component(string Type, JsonObject Data, Schema Schema)
{
public const string Discriminator = "schemaId";
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs

@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Core
{ {
public static readonly InvariantPartitioning Instance = new InvariantPartitioning(); public static readonly InvariantPartitioning Instance = new InvariantPartitioning();
public static readonly string Key = "iv"; public static readonly string Key = "iv";
public static readonly string Name = "Invariant";
public static readonly string Description = "invariant value";
public string Master public string Master
{ {
@ -28,7 +30,7 @@ namespace Squidex.Domain.Apps.Core
{ {
if (Contains(key)) if (Contains(key))
{ {
return "Invariant"; return Name;
} }
return null; return null;
@ -61,7 +63,7 @@ namespace Squidex.Domain.Apps.Core
public override string ToString() public override string ToString()
{ {
return "invariant value"; return Description;
} }
} }
} }

57
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentFieldProperties.cs

@ -0,0 +1,57 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed record ComponentFieldProperties : FieldProperties
{
public DomainId SchemaId
{
init
{
if (value != default)
{
SchemaIds = ImmutableList.Create(value);
}
else
{
SchemaIds = null;
}
}
get
{
return SchemaIds?.FirstOrDefault() ?? default;
}
}
public ImmutableList<DomainId>? SchemaIds { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{
return visitor.Visit(this, args);
}
public override T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, IField field, TArgs args)
{
return visitor.Visit((IField<ComponentFieldProperties>)field, args);
}
public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null)
{
return Fields.Component(id, name, partitioning, this, settings);
}
public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null)
{
return Fields.Component(id, name, this, settings);
}
}
}

61
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed record ComponentsFieldProperties : FieldProperties
{
public int? MinItems { get; init; }
public int? MaxItems { get; init; }
public DomainId SchemaId
{
init
{
if (value != default)
{
SchemaIds = ImmutableList.Create(value);
}
else
{
SchemaIds = null;
}
}
get
{
return SchemaIds?.FirstOrDefault() ?? default;
}
}
public ImmutableList<DomainId>? SchemaIds { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{
return visitor.Visit(this, args);
}
public override T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, IField field, TArgs args)
{
return visitor.Visit((IField<ComponentsFieldProperties>)field, args);
}
public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null)
{
return Fields.Components(id, name, partitioning, this, settings);
}
public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null)
{
return Fields.Components(id, name, this, settings);
}
}
}

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

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public abstract class FieldBase public abstract class FieldBase : IMetadataProvider
{ {
private Dictionary<string, object> metadata; private Dictionary<string, object> metadata;

44
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs

@ -6,8 +6,10 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using NamedIdStatic = Squidex.Infrastructure.NamedId; using NamedIdStatic = Squidex.Infrastructure.NamedId;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
@ -24,6 +26,48 @@ namespace Squidex.Domain.Apps.Core.Schemas
return fields.Where(x => IsForApi(x, withHidden)); return fields.Where(x => IsForApi(x, withHidden));
} }
public static IEnumerable<IRootField> GetSharedFields(this IField field, ImmutableList<DomainId>? schemaIds, bool withHidden)
{
if (schemaIds == null || schemaIds.Count == 0)
{
return Enumerable.Empty<IRootField>();
}
var allFields =
schemaIds
.Select(x => field.GetResolvedSchema(x)).NotNull()
.SelectMany(x => x.Fields.ForApi(withHidden))
.GroupBy(x => new { x.Name, Type = x.RawProperties.GetType() }).Where(x => x.Count() == 1)
.Select(x => x.First());
return allFields;
}
public static T SetResolvedSchema<T>(this T metadataProvider, DomainId id, Schema schema) where T : IMetadataProvider
{
var key = $"ResolvedSchema_{id}";
metadataProvider.Metadata[key] = schema;
return metadataProvider;
}
public static Schema? GetResolvedSchema<T>(this T metadataProvider, object id) where T : IMetadataProvider
{
var key = $"ResolvedSchema_{id}";
return metadataProvider.GetMetadata<Schema>(key);
}
public static bool TryGetResolvedSchema<T>(this T metadataProvider, object id, [MaybeNullWhen(false)] out Schema schema) where T : IMetadataProvider
{
var key = $"ResolvedSchema_{id}";
schema = metadataProvider.GetMetadata<Schema>(key);
return schema != null;
}
public static bool IsForApi<T>(this T field, bool withHidden = false) where T : IField public static bool IsForApi<T>(this T field, bool withHidden = false) where T : IField
{ {
return (withHidden || !field.IsHidden) && !field.RawProperties.IsUIProperty(); return (withHidden || !field.IsHidden) && !field.RawProperties.IsUIProperty();

168
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs

@ -16,112 +16,158 @@ namespace Squidex.Domain.Apps.Core.Schemas
return new ArrayField(id, name, partitioning, fields); return new ArrayField(id, name, partitioning, fields);
} }
public static ArrayField Array(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField Array(long id, string name, Partitioning partitioning,
ArrayFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new ArrayField(id, name, partitioning, properties, settings); return new ArrayField(id, name, partitioning, properties, settings);
} }
public static RootField<AssetsFieldProperties> Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<AssetsFieldProperties> Assets(long id, string name, Partitioning partitioning,
AssetsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<AssetsFieldProperties>(id, name, partitioning, properties, settings); return new RootField<AssetsFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<BooleanFieldProperties> Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<BooleanFieldProperties> Boolean(long id, string name, Partitioning partitioning,
BooleanFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<BooleanFieldProperties>(id, name, partitioning, properties, settings); return new RootField<BooleanFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<DateTimeFieldProperties> DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<ComponentFieldProperties> Component(long id, string name, Partitioning partitioning,
ComponentFieldProperties? properties = null, IFieldSettings? settings = null)
{
return new RootField<ComponentFieldProperties>(id, name, partitioning, properties, settings);
}
public static RootField<ComponentsFieldProperties> Components(long id, string name, Partitioning partitioning,
ComponentsFieldProperties? properties = null, IFieldSettings? settings = null)
{
return new RootField<ComponentsFieldProperties>(id, name, partitioning, properties, settings);
}
public static RootField<DateTimeFieldProperties> DateTime(long id, string name, Partitioning partitioning,
DateTimeFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<DateTimeFieldProperties>(id, name, partitioning, properties, settings); return new RootField<DateTimeFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<GeolocationFieldProperties> Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<GeolocationFieldProperties> Geolocation(long id, string name, Partitioning partitioning,
GeolocationFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<GeolocationFieldProperties>(id, name, partitioning, properties, settings); return new RootField<GeolocationFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<JsonFieldProperties> Json(long id, string name, Partitioning partitioning, JsonFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<JsonFieldProperties> Json(long id, string name, Partitioning partitioning,
JsonFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<JsonFieldProperties>(id, name, partitioning, properties, settings); return new RootField<JsonFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<NumberFieldProperties> Number(long id, string name, Partitioning partitioning, NumberFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<NumberFieldProperties> Number(long id, string name, Partitioning partitioning,
NumberFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<NumberFieldProperties>(id, name, partitioning, properties, settings); return new RootField<NumberFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<ReferencesFieldProperties> References(long id, string name, Partitioning partitioning, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<ReferencesFieldProperties> References(long id, string name, Partitioning partitioning,
ReferencesFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<ReferencesFieldProperties>(id, name, partitioning, properties, settings); return new RootField<ReferencesFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<StringFieldProperties> String(long id, string name, Partitioning partitioning, StringFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<StringFieldProperties> String(long id, string name, Partitioning partitioning,
StringFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<StringFieldProperties>(id, name, partitioning, properties, settings); return new RootField<StringFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<TagsFieldProperties> Tags(long id, string name, Partitioning partitioning, TagsFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<TagsFieldProperties> Tags(long id, string name, Partitioning partitioning,
TagsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<TagsFieldProperties>(id, name, partitioning, properties, settings); return new RootField<TagsFieldProperties>(id, name, partitioning, properties, settings);
} }
public static RootField<UIFieldProperties> UI(long id, string name, Partitioning partitioning, UIFieldProperties? properties = null, IFieldSettings? settings = null) public static RootField<UIFieldProperties> UI(long id, string name, Partitioning partitioning,
UIFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new RootField<UIFieldProperties>(id, name, partitioning, properties, settings); return new RootField<UIFieldProperties>(id, name, partitioning, properties, settings);
} }
public static NestedField<AssetsFieldProperties> Assets(long id, string name, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<AssetsFieldProperties> Assets(long id, string name,
AssetsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<AssetsFieldProperties>(id, name, properties, settings); return new NestedField<AssetsFieldProperties>(id, name, properties, settings);
} }
public static NestedField<BooleanFieldProperties> Boolean(long id, string name, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<BooleanFieldProperties> Boolean(long id, string name,
BooleanFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<BooleanFieldProperties>(id, name, properties, settings); return new NestedField<BooleanFieldProperties>(id, name, properties, settings);
} }
public static NestedField<DateTimeFieldProperties> DateTime(long id, string name, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<ComponentFieldProperties> Component(long id, string name,
ComponentFieldProperties? properties = null, IFieldSettings? settings = null)
{
return new NestedField<ComponentFieldProperties>(id, name, properties, settings);
}
public static NestedField<ComponentsFieldProperties> Components(long id, string name,
ComponentsFieldProperties? properties = null, IFieldSettings? settings = null)
{
return new NestedField<ComponentsFieldProperties>(id, name, properties, settings);
}
public static NestedField<DateTimeFieldProperties> DateTime(long id, string name,
DateTimeFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<DateTimeFieldProperties>(id, name, properties, settings); return new NestedField<DateTimeFieldProperties>(id, name, properties, settings);
} }
public static NestedField<GeolocationFieldProperties> Geolocation(long id, string name, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<GeolocationFieldProperties> Geolocation(long id, string name,
GeolocationFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<GeolocationFieldProperties>(id, name, properties, settings); return new NestedField<GeolocationFieldProperties>(id, name, properties, settings);
} }
public static NestedField<JsonFieldProperties> Json(long id, string name, JsonFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<JsonFieldProperties> Json(long id, string name,
JsonFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<JsonFieldProperties>(id, name, properties, settings); return new NestedField<JsonFieldProperties>(id, name, properties, settings);
} }
public static NestedField<NumberFieldProperties> Number(long id, string name, NumberFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<NumberFieldProperties> Number(long id, string name,
NumberFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<NumberFieldProperties>(id, name, properties, settings); return new NestedField<NumberFieldProperties>(id, name, properties, settings);
} }
public static NestedField<ReferencesFieldProperties> References(long id, string name, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<ReferencesFieldProperties> References(long id, string name,
ReferencesFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<ReferencesFieldProperties>(id, name, properties, settings); return new NestedField<ReferencesFieldProperties>(id, name, properties, settings);
} }
public static NestedField<StringFieldProperties> String(long id, string name, StringFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<StringFieldProperties> String(long id, string name,
StringFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<StringFieldProperties>(id, name, properties, settings); return new NestedField<StringFieldProperties>(id, name, properties, settings);
} }
public static NestedField<TagsFieldProperties> Tags(long id, string name, TagsFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<TagsFieldProperties> Tags(long id, string name,
TagsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<TagsFieldProperties>(id, name, properties, settings); return new NestedField<TagsFieldProperties>(id, name, properties, settings);
} }
public static NestedField<UIFieldProperties> UI(long id, string name, UIFieldProperties? properties = null, IFieldSettings? settings = null) public static NestedField<UIFieldProperties> UI(long id, string name,
UIFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return new NestedField<UIFieldProperties>(id, name, properties, settings); return new NestedField<UIFieldProperties>(id, name, properties, settings);
} }
public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func<ArrayField, ArrayField>? handler = null, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning,
Func<ArrayField, ArrayField>? handler = null, ArrayFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
var field = Array(id, name, partitioning, properties, settings); var field = Array(id, name, partitioning, properties, settings);
@ -133,102 +179,140 @@ namespace Squidex.Domain.Apps.Core.Schemas
return schema.AddField(field); return schema.AddField(field);
} }
public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning,
AssetsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(Assets(id, name, partitioning, properties, settings)); return schema.AddField(Assets(id, name, partitioning, properties, settings));
} }
public static Schema AddBoolean(this Schema schema, long id, string name, Partitioning partitioning, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddBoolean(this Schema schema, long id, string name, Partitioning partitioning,
BooleanFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(Boolean(id, name, partitioning, properties, settings)); return schema.AddField(Boolean(id, name, partitioning, properties, settings));
} }
public static Schema AddDateTime(this Schema schema, long id, string name, Partitioning partitioning, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddComponent(this Schema schema, long id, string name, Partitioning partitioning,
ComponentFieldProperties? properties = null, IFieldSettings? settings = null)
{
return schema.AddField(Component(id, name, partitioning, properties, settings));
}
public static Schema AddComponents(this Schema schema, long id, string name, Partitioning partitioning,
ComponentsFieldProperties? properties = null, IFieldSettings? settings = null)
{
return schema.AddField(Components(id, name, partitioning, properties, settings));
}
public static Schema AddDateTime(this Schema schema, long id, string name, Partitioning partitioning,
DateTimeFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(DateTime(id, name, partitioning, properties, settings)); return schema.AddField(DateTime(id, name, partitioning, properties, settings));
} }
public static Schema AddGeolocation(this Schema schema, long id, string name, Partitioning partitioning, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddGeolocation(this Schema schema, long id, string name, Partitioning partitioning,
GeolocationFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(Geolocation(id, name, partitioning, properties, settings)); return schema.AddField(Geolocation(id, name, partitioning, properties, settings));
} }
public static Schema AddJson(this Schema schema, long id, string name, Partitioning partitioning, JsonFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddJson(this Schema schema, long id, string name, Partitioning partitioning,
JsonFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(Json(id, name, partitioning, properties, settings)); return schema.AddField(Json(id, name, partitioning, properties, settings));
} }
public static Schema AddNumber(this Schema schema, long id, string name, Partitioning partitioning, NumberFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddNumber(this Schema schema, long id, string name, Partitioning partitioning,
NumberFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(Number(id, name, partitioning, properties, settings)); return schema.AddField(Number(id, name, partitioning, properties, settings));
} }
public static Schema AddReferences(this Schema schema, long id, string name, Partitioning partitioning, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddReferences(this Schema schema, long id, string name, Partitioning partitioning,
ReferencesFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(References(id, name, partitioning, properties, settings)); return schema.AddField(References(id, name, partitioning, properties, settings));
} }
public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning, StringFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning,
StringFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(String(id, name, partitioning, properties, settings)); return schema.AddField(String(id, name, partitioning, properties, settings));
} }
public static Schema AddTags(this Schema schema, long id, string name, Partitioning partitioning, TagsFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddTags(this Schema schema, long id, string name, Partitioning partitioning,
TagsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(Tags(id, name, partitioning, properties, settings)); return schema.AddField(Tags(id, name, partitioning, properties, settings));
} }
public static Schema AddUI(this Schema schema, long id, string name, Partitioning partitioning, UIFieldProperties? properties = null, IFieldSettings? settings = null) public static Schema AddUI(this Schema schema, long id, string name, Partitioning partitioning,
UIFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return schema.AddField(UI(id, name, partitioning, properties, settings)); return schema.AddField(UI(id, name, partitioning, properties, settings));
} }
public static ArrayField AddAssets(this ArrayField field, long id, string name, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddAssets(this ArrayField field, long id, string name,
AssetsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(Assets(id, name, properties, settings)); return field.AddField(Assets(id, name, properties, settings));
} }
public static ArrayField AddBoolean(this ArrayField field, long id, string name, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddBoolean(this ArrayField field, long id, string name,
BooleanFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(Boolean(id, name, properties, settings)); return field.AddField(Boolean(id, name, properties, settings));
} }
public static ArrayField AddDateTime(this ArrayField field, long id, string name, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddComponents(this ArrayField field, long id, string name,
ComponentFieldProperties? properties = null, IFieldSettings? settings = null)
{
return field.AddField(Component(id, name, properties, settings));
}
public static ArrayField AddDateTime(this ArrayField field, long id, string name,
DateTimeFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(DateTime(id, name, properties, settings)); return field.AddField(DateTime(id, name, properties, settings));
} }
public static ArrayField AddGeolocation(this ArrayField field, long id, string name, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddGeolocation(this ArrayField field, long id, string name,
GeolocationFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(Geolocation(id, name, properties, settings)); return field.AddField(Geolocation(id, name, properties, settings));
} }
public static ArrayField AddJson(this ArrayField field, long id, string name, JsonFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddJson(this ArrayField field, long id, string name,
JsonFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(Json(id, name, properties, settings)); return field.AddField(Json(id, name, properties, settings));
} }
public static ArrayField AddNumber(this ArrayField field, long id, string name, NumberFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddNumber(this ArrayField field, long id, string name,
NumberFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(Number(id, name, properties, settings)); return field.AddField(Number(id, name, properties, settings));
} }
public static ArrayField AddReferences(this ArrayField field, long id, string name, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddReferences(this ArrayField field, long id, string name,
ReferencesFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(References(id, name, properties, settings)); return field.AddField(References(id, name, properties, settings));
} }
public static ArrayField AddString(this ArrayField field, long id, string name, StringFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddString(this ArrayField field, long id, string name,
StringFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(String(id, name, properties, settings)); return field.AddField(String(id, name, properties, settings));
} }
public static ArrayField AddTags(this ArrayField field, long id, string name, TagsFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddTags(this ArrayField field, long id, string name,
TagsFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(Tags(id, name, properties, settings)); return field.AddField(Tags(id, name, properties, settings));
} }
public static ArrayField AddUI(this ArrayField field, long id, string name, UIFieldProperties? properties = null, IFieldSettings? settings = null) public static ArrayField AddUI(this ArrayField field, long id, string name,
UIFieldProperties? properties = null, IFieldSettings? settings = null)
{ {
return field.AddField(UI(id, name, properties, settings)); return field.AddField(UI(id, name, properties, settings));
} }

4
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public interface IField : IFieldSettings public interface IField : IFieldSettings, IMetadataProvider
{ {
long Id { get; } long Id { get; }
@ -17,4 +17,4 @@ namespace Squidex.Domain.Apps.Core.Schemas
T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, TArgs args); T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, TArgs args);
} }
} }

4
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs

@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
T Visit(BooleanFieldProperties properties, TArgs args); T Visit(BooleanFieldProperties properties, TArgs args);
T Visit(ComponentFieldProperties properties, TArgs args);
T Visit(ComponentsFieldProperties properties, TArgs args);
T Visit(DateTimeFieldProperties properties, TArgs args); T Visit(DateTimeFieldProperties properties, TArgs args);
T Visit(GeolocationFieldProperties properties, TArgs args); T Visit(GeolocationFieldProperties properties, TArgs args);

4
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs

@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
T Visit(IField<BooleanFieldProperties> field, TArgs args); T Visit(IField<BooleanFieldProperties> field, TArgs args);
T Visit(IField<ComponentFieldProperties> field, TArgs args);
T Visit(IField<ComponentsFieldProperties> field, TArgs args);
T Visit(IField<DateTimeFieldProperties> field, TArgs args); T Visit(IField<DateTimeFieldProperties> field, TArgs args);
T Visit(IField<GeolocationFieldProperties> field, TArgs args); T Visit(IField<GeolocationFieldProperties> field, TArgs args);

23
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IMetadataProvider.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IMetadataProvider
{
IDictionary<string, object> Metadata { get; }
T? GetMetadata<T>(string key, T? defaultValue = default);
T GetMetadata<T>(string key, Func<T> defaultValueFactory);
bool HasMetadata(string key);
}
}

3
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaType.cs

@ -10,6 +10,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public enum SchemaType public enum SchemaType
{ {
Default, Default,
Singleton Singleton,
Component
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
if (newData != null) if (newData != null)
{ {
newData = ConvertData(converters, field, newData); newData = ConvertData(field, newData, converters);
} }
if (newData != null) if (newData != null)
@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
return result; return result;
} }
private static ContentFieldData? ConvertData(FieldConverter[] converters, IRootField field, ContentFieldData data) private static ContentFieldData? ConvertData(IRootField field, ContentFieldData data, FieldConverter[] converters)
{ {
if (converters != null) if (converters != null)
{ {
@ -60,4 +60,4 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
return data; return data;
} }
} }
} }

165
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs

@ -180,32 +180,171 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
return (data, field) => return (data, field) =>
{ {
foreach (var (key, value) in data.ToList()) ContentFieldData? newData = null;
foreach (var (key, value) in data)
{ {
var newValue = value; var newValue = ConvertByType(field, value, null, converters);
for (var i = 0; i < converters.Length; i++) if (newValue == null)
{
newData ??= new ContentFieldData(data);
newData.Remove(key);
}
else if (!ReferenceEquals(newValue, value))
{ {
newValue = converters[i](newValue!, field, null); newData ??= new ContentFieldData(data);
newData[key] = newValue;
}
}
if (newValue == null) return newData ?? data;
{ };
break; }
}
private static IJsonValue? ConvertByType<T>(T field, IJsonValue? value, IArrayField? parent, ValueConverter[] converters) where T : IField
{
switch (field)
{
case IArrayField arrayField:
return ConvertArray(arrayField, value, converters);
case IField<ComponentFieldProperties>:
return ConvertComponent(field, value, converters);
case IField<ComponentsFieldProperties>:
return ConvertComponents(field, value, converters);
default:
return ConvertValue(field, value, parent, converters);
}
}
private static IJsonValue? ConvertArray(IArrayField field, IJsonValue? value, ValueConverter[] converters)
{
if (value is JsonArray array)
{
JsonArray? result = null;
for (int i = 0, j = 0; i < array.Count; i++, j++)
{
var newValue = ConvertArrayItem(field, array[i], converters);
if (newValue == null)
{
result ??= new JsonArray(array);
result.RemoveAt(j);
j--;
} }
else if (!ReferenceEquals(newValue, array[i]))
{
result ??= new JsonArray(array);
result[j] = newValue;
}
}
return result ?? array;
}
return null;
}
private static IJsonValue? ConvertComponents(IField field, IJsonValue? value, ValueConverter[] converters)
{
if (value is JsonArray array)
{
JsonArray? result = null;
for (int i = 0, j = 0; i < array.Count; i++, j++)
{
var newValue = ConvertComponent(field, array[i], converters);
if (newValue == null) if (newValue == null)
{ {
data.Remove(key); result ??= new JsonArray(array);
result.RemoveAt(j);
j--;
} }
else if (!ReferenceEquals(newValue, value)) else if (!ReferenceEquals(newValue, array[i]))
{ {
data[key] = newValue; result ??= new JsonArray(array);
result[j] = newValue;
} }
} }
return data; return result ?? array;
}; }
return null;
}
private static IJsonValue? ConvertComponent(IField field, IJsonValue? value, ValueConverter[] converters)
{
if (value is JsonObject obj && obj.TryGetValue<JsonString>(Component.Discriminator, out var type) && field.TryGetResolvedSchema(type.Value, out var schema))
{
return ConvertNested(schema.FieldCollection, obj, null, converters);
}
return null;
}
private static IJsonValue? ConvertArrayItem(IArrayField field, IJsonValue? value, ValueConverter[] converters)
{
if (value is JsonObject obj)
{
return ConvertNested(field.FieldCollection, obj, field, converters);
}
return null;
}
private static IJsonValue ConvertNested<T>(FieldCollection<T> fields, JsonObject source, IArrayField? parent, ValueConverter[] converters) where T : IField
{
JsonObject? result = null;
foreach (var (key, value) in source)
{
var newValue = value;
if (fields.ByName.TryGetValue(key, out var field))
{
newValue = ConvertByType(field, value, parent, converters);
}
else if (key != Component.Discriminator)
{
newValue = null;
}
if (newValue == null)
{
result ??= new JsonObject(source);
result.Remove(key);
}
else if (!ReferenceEquals(newValue, value))
{
result ??= new JsonObject(source);
result[key] = newValue;
}
}
return result ?? source;
}
private static IJsonValue? ConvertValue(IField field, IJsonValue? value, IArrayField? parent, ValueConverter[] converters)
{
var newValue = value;
for (var i = 0; i < converters.Length; i++)
{
newValue = converters[i](newValue!, field, parent);
if (newValue == null)
{
break;
}
}
return newValue;
} }
} }
} }

10
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs

@ -65,6 +65,16 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
} }
} }
public string Visit(ComponentFieldProperties properties, Args args)
{
return "{ Component }";
}
public string Visit(ComponentsFieldProperties properties, Args args)
{
return FormatArray(args.Value, "Component", "Components");
}
public string Visit(DateTimeFieldProperties properties, Args args) public string Visit(DateTimeFieldProperties properties, Args args)
{ {
return args.Value.ToString(); return args.Value.ToString();

52
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
public delegate IJsonValue? ValueConverter(IJsonValue value, IField field, IArrayField? parent = null); public delegate IJsonValue? ValueConverter(IJsonValue value, IField field, IArrayField? parent);
public static class ValueConverters public static class ValueConverters
{ {
@ -110,55 +110,5 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
return value; return value;
}; };
} }
public static ValueConverter ForNested(params ValueConverter[] converters)
{
if (converters?.Any() != true)
{
return Noop;
}
return (value, field, parent) =>
{
if (value is JsonArray array && field is IArrayField arrayField)
{
foreach (var nested in array.OfType<JsonObject>())
{
foreach (var (fieldName, nestedValue) in nested.ToList())
{
var newValue = nestedValue;
if (arrayField.FieldsByName.TryGetValue(fieldName, out var nestedField))
{
for (var i = 0; i < converters.Length; i++)
{
newValue = converters[i](newValue!, nestedField, arrayField);
if (newValue == null)
{
break;
}
}
}
else
{
newValue = null;
}
if (newValue == null)
{
nested.Remove(fieldName);
}
else if (!ReferenceEquals(nestedValue, newValue))
{
nested[fieldName] = newValue;
}
}
}
}
return value;
};
}
} }
} }

10
backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs

@ -63,6 +63,16 @@ namespace Squidex.Domain.Apps.Core.DefaultValues
return JsonValue.Create(value); return JsonValue.Create(value);
} }
public IJsonValue Visit(ComponentFieldProperties properties, Args args)
{
return JsonValue.Null;
}
public IJsonValue Visit(ComponentsFieldProperties properties, Args args)
{
return JsonValue.Array();
}
public IJsonValue Visit(GeolocationFieldProperties properties, Args args) public IJsonValue Visit(GeolocationFieldProperties properties, Args args)
{ {
return JsonValue.Null; return JsonValue.Null;

10
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs

@ -56,6 +56,16 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
return args.Value; return args.Value;
} }
public IJsonValue Visit(IField<ComponentFieldProperties> field, Args args)
{
return args.Value;
}
public IJsonValue Visit(IField<ComponentsFieldProperties> field, Args args)
{
return args.Value;
}
public IJsonValue Visit(IField<DateTimeFieldProperties> field, Args args) public IJsonValue Visit(IField<DateTimeFieldProperties> field, Args args)
{ {
return args.Value; return args.Value;

60
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs

@ -6,7 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -48,15 +48,9 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
if (args.Value is JsonArray array) if (args.Value is JsonArray array)
{ {
foreach (var item in array.OfType<JsonObject>()) for (var i = 0; i < array.Count; i++)
{ {
foreach (var nestedField in field.Fields) ExtractFromArrayItem(field, array[i], args);
{
if (item.TryGetValue(nestedField.Name, out var nestedValue))
{
nestedField.Accept(this, new Args(nestedValue, args.Result, args.ResultLimit));
}
}
} }
} }
@ -82,6 +76,26 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
return None.Value; return None.Value;
} }
public None Visit(IField<ComponentFieldProperties> field, Args args)
{
ExtractFromComponent(field, args.Value, args);
return None.Value;
}
public None Visit(IField<ComponentsFieldProperties> field, Args args)
{
if (args.Value is JsonArray array)
{
for (var i = 0; i < array.Count; i++)
{
ExtractFromComponent(field, array[i], args);
}
}
return None.Value;
}
public None Visit(IField<DateTimeFieldProperties> field, Args args) public None Visit(IField<DateTimeFieldProperties> field, Args args)
{ {
return None.Value; return None.Value;
@ -117,6 +131,34 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
return None.Value; return None.Value;
} }
private void ExtractFromArrayItem(IArrayField field, IJsonValue value, Args args)
{
if (value is JsonObject obj)
{
foreach (var nestedField in field.Fields)
{
if (obj.TryGetValue(nestedField.Name, out var nestedValue))
{
nestedField.Accept(this, new Args(nestedValue, args.Result, args.ResultLimit));
}
}
}
}
private void ExtractFromComponent(IField field, IJsonValue value, Args args)
{
if (value is JsonObject obj && obj.TryGetValue<JsonString>(Component.Discriminator, out var type) && field.TryGetResolvedSchema(type.Value, out var schema))
{
foreach (var componentField in schema.Fields)
{
if (obj.TryGetValue(componentField.Name, out var componentValue))
{
componentField.Accept(this, new Args(componentValue, args.Result, args.ResultLimit));
}
}
}
}
private static void AddIds(ref Args args) private static void AddIds(ref Args args)
{ {
var added = 0; var added = 0;

69
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Text; using Squidex.Text;
@ -13,6 +14,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{ {
internal sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference?, EdmTypeVisitor.Args> internal sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference?, EdmTypeVisitor.Args>
{ {
private const int MaxDepth = 5;
private static readonly EdmComplexType JsonType = new EdmComplexType("Squidex", "Json", null, false, true); private static readonly EdmComplexType JsonType = new EdmComplexType("Squidex", "Json", null, false, true);
private static readonly EdmTypeVisitor Instance = new EdmTypeVisitor(); private static readonly EdmTypeVisitor Instance = new EdmTypeVisitor();
@ -20,9 +22,18 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{ {
public readonly EdmTypeFactory Factory; public readonly EdmTypeFactory Factory;
public Args(EdmTypeFactory factory) public readonly int Level;
public Args(EdmTypeFactory factory, int level)
{ {
Factory = factory; Factory = factory;
Level = level;
}
public Args Increment()
{
return new Args(Factory, Level + 1);
} }
} }
@ -32,29 +43,14 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
public static IEdmTypeReference? BuildType(IField field, EdmTypeFactory factory) public static IEdmTypeReference? BuildType(IField field, EdmTypeFactory factory)
{ {
var args = new Args(factory); var args = new Args(factory, 0);
return field.Accept(Instance, args); return field.Accept(Instance, args);
} }
public IEdmTypeReference? Visit(IArrayField field, Args args) public IEdmTypeReference? Visit(IArrayField field, Args args)
{ {
var (fieldEdmType, created) = args.Factory($"Data.{field.Name.ToPascalCase()}.Item"); return CreateNestedType(field, field.Fields.ForApi(true), args);
if (created)
{
foreach (var nestedField in field.Fields)
{
var nestedEdmType = nestedField.Accept(this, args);
if (nestedEdmType != null)
{
fieldEdmType.AddStructuralProperty(nestedField.Name.EscapeEdmField(), nestedEdmType);
}
}
}
return new EdmComplexTypeReference(fieldEdmType, false);
} }
public IEdmTypeReference? Visit(IField<AssetsFieldProperties> field, Args args) public IEdmTypeReference? Visit(IField<AssetsFieldProperties> field, Args args)
@ -67,6 +63,16 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field); return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field);
} }
public IEdmTypeReference? Visit(IField<ComponentFieldProperties> field, Args args)
{
return CreateNestedType(field, field.GetSharedFields(field.Properties.SchemaIds, true), args);
}
public IEdmTypeReference? Visit(IField<ComponentsFieldProperties> field, Args args)
{
return CreateNestedType(field, field.GetSharedFields(field.Properties.SchemaIds, true), args);
}
public IEdmTypeReference? Visit(IField<DateTimeFieldProperties> field, Args args) public IEdmTypeReference? Visit(IField<DateTimeFieldProperties> field, Args args)
{ {
return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field); return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field);
@ -121,5 +127,32 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{ {
return new EdmComplexTypeReference(JsonType, !field.RawProperties.IsRequired); return new EdmComplexTypeReference(JsonType, !field.RawProperties.IsRequired);
} }
private IEdmTypeReference? CreateNestedType(IField field, IEnumerable<IField> nested, Args args)
{
if (args.Level > MaxDepth)
{
return null;
}
var (fieldEdmType, created) = args.Factory($"Data.{field.Name.ToPascalCase()}.Nested");
if (created)
{
var nestedArgs = args.Increment();
foreach (var sharedField in nested)
{
var nestedEdmType = sharedField.Accept(this, nestedArgs);
if (nestedEdmType != null)
{
fieldEdmType.AddStructuralProperty(sharedField.Name.EscapeEdmField(), nestedEdmType);
}
}
}
return new EdmComplexTypeReference(fieldEdmType, false);
}
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
foreach (var field in schema.Fields.ForApi()) foreach (var field in schema.Fields.ForApi())
{ {
var property = JsonTypeVisitor.BuildProperty(field, null, false); var property = JsonTypeVisitor.BuildProperty(field);
if (property != null) if (property != null)
{ {
@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
foreach (var field in schema.Fields.ForApi(withHidden)) foreach (var field in schema.Fields.ForApi(withHidden))
{ {
var propertyItem = JsonTypeVisitor.BuildProperty(field, null, withHidden); var propertyItem = JsonTypeVisitor.BuildProperty(field, schemaResolver, withHidden);
if (propertyItem != null) if (propertyItem != null)
{ {
@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
foreach (var partitionKey in partitioning.AllKeys) foreach (var partitionKey in partitioning.AllKeys)
{ {
var propertyItem = JsonTypeVisitor.BuildProperty(field, null, withHidden); var propertyItem = JsonTypeVisitor.BuildProperty(field, withHiddenFields: withHidden);
if (propertyItem != null) if (propertyItem != null)
{ {

141
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs

@ -7,8 +7,12 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using NJsonSchema; using NJsonSchema;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
@ -17,6 +21,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
internal sealed class JsonTypeVisitor : IFieldVisitor<JsonSchemaProperty?, JsonTypeVisitor.Args> internal sealed class JsonTypeVisitor : IFieldVisitor<JsonSchemaProperty?, JsonTypeVisitor.Args>
{ {
private const int MaxDepth = 5;
private static readonly JsonTypeVisitor Instance = new JsonTypeVisitor(); private static readonly JsonTypeVisitor Instance = new JsonTypeVisitor();
public readonly struct Args public readonly struct Args
@ -25,11 +30,20 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public readonly bool WithHiddenFields; public readonly bool WithHiddenFields;
public Args(SchemaResolver? schemaResolver, bool withHiddenFields) public readonly int Level;
public Args(SchemaResolver? schemaResolver, bool withHiddenFields, int level)
{ {
SchemaResolver = schemaResolver; SchemaResolver = schemaResolver;
WithHiddenFields = withHiddenFields; WithHiddenFields = withHiddenFields;
Level = level;
}
public Args Increment()
{
return new Args(SchemaResolver, WithHiddenFields, Level + 1);
} }
} }
@ -37,20 +51,27 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{ {
} }
public static JsonSchemaProperty? BuildProperty(IField field, SchemaResolver? schemaResolver, bool withHiddenFields) public static JsonSchemaProperty? BuildProperty(IField field, SchemaResolver? schemaResolver = null, bool withHiddenFields = false)
{ {
var args = new Args(schemaResolver, withHiddenFields); var args = new Args(schemaResolver, withHiddenFields, 0);
return field.Accept(Instance, args); return field.Accept(Instance, args);
} }
public JsonSchemaProperty? Visit(IArrayField field, Args args) public JsonSchemaProperty? Visit(IArrayField field, Args args)
{ {
if (args.Level > MaxDepth)
{
return null;
}
var itemSchema = SchemaBuilder.Object(); var itemSchema = SchemaBuilder.Object();
var nestedArgs = args.Increment();
foreach (var nestedField in field.Fields.ForApi(args.WithHiddenFields)) foreach (var nestedField in field.Fields.ForApi(args.WithHiddenFields))
{ {
var nestedProperty = nestedField.Accept(this, args); var nestedProperty = nestedField.Accept(this, nestedArgs);
if (nestedProperty != null) if (nestedProperty != null)
{ {
@ -74,48 +95,46 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
return SchemaBuilder.BooleanProperty(); return SchemaBuilder.BooleanProperty();
} }
public JsonSchemaProperty? Visit(IField<DateTimeFieldProperties> field, Args args) public JsonSchemaProperty? Visit(IField<ComponentFieldProperties> field, Args args)
{ {
return SchemaBuilder.DateTimeProperty(); if (args.Level > MaxDepth)
{
return null;
}
var property = SchemaBuilder.ObjectProperty();
BuildComponent(property, field, field.Properties.SchemaIds, args);
return property;
} }
public JsonSchemaProperty? Visit(IField<GeolocationFieldProperties> field, Args args) public JsonSchemaProperty? Visit(IField<ComponentsFieldProperties> field, Args args)
{ {
if (args.SchemaResolver != null) if (args.Level > MaxDepth)
{ {
var reference = args.SchemaResolver("GeolocationDto", () => return null;
{ }
var geolocationSchema = SchemaBuilder.Object();
geolocationSchema.Format = GeoJson.Format; var itemSchema = SchemaBuilder.Object();
geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty BuildComponent(itemSchema, field, field.Properties.SchemaIds, args);
{
Type = JsonObjectType.Number,
Maximum = 90,
Minimum = -90
}.SetRequired(false));
geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty return SchemaBuilder.ArrayProperty(itemSchema);
{ }
Type = JsonObjectType.Number,
Maximum = 180,
Minimum = -180
}.SetRequired(false));
return geolocationSchema; public JsonSchemaProperty? Visit(IField<DateTimeFieldProperties> field, Args args)
}); {
return SchemaBuilder.DateTimeProperty();
}
return SchemaBuilder.ObjectProperty(reference); public JsonSchemaProperty? Visit(IField<GeolocationFieldProperties> field, Args args)
} {
else var property = SchemaBuilder.ObjectProperty();
{
var property = SchemaBuilder.ObjectProperty();
property.Format = GeoJson.Format; property.Format = GeoJson.Format;
return property; return property;
}
} }
public JsonSchemaProperty? Visit(IField<JsonFieldProperties> field, Args args) public JsonSchemaProperty? Visit(IField<JsonFieldProperties> field, Args args)
@ -176,5 +195,59 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{ {
return null; return null;
} }
private void BuildComponent(JsonSchema jsonSchema, IField field, ImmutableList<DomainId>? schemaIds, Args args)
{
jsonSchema.Properties.Add(Component.Discriminator, SchemaBuilder.StringProperty(isRequired: true));
if (args.SchemaResolver != null)
{
var schemas = schemaIds?.Select(x => field.GetResolvedSchema(x)).NotNull() ?? Enumerable.Empty<Schema>();
var discriminator = new OpenApiDiscriminator
{
PropertyName = Component.Discriminator
};
foreach (var schema in schemas)
{
var schemaName = $"{schema.TypeName()}ComponentDto";
var componentSchema = args.SchemaResolver(schemaName, () =>
{
var nestedArgs = args.Increment();
var componentSchema = SchemaBuilder.Object();
foreach (var sharedField in schema.Fields.ForApi(nestedArgs.WithHiddenFields))
{
var sharedProperty = sharedField.Accept(this, nestedArgs);
if (sharedProperty != null)
{
sharedProperty.Description = sharedField.RawProperties.Hints;
sharedProperty.SetRequired(sharedField.RawProperties.IsRequired);
componentSchema.Properties.Add(sharedField.Name, sharedProperty);
}
}
componentSchema.Properties.Add(Component.Discriminator, SchemaBuilder.StringProperty(isRequired: true));
return componentSchema;
});
jsonSchema.OneOf.Add(componentSchema);
discriminator.Mapping[schemaName] = componentSchema;
}
jsonSchema.DiscriminatorObject = discriminator;
}
else
{
jsonSchema.AllowAdditionalProperties = true;
}
}
} }
} }

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

@ -11,8 +11,6 @@ using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class ScriptVars : ScriptContext public sealed class ScriptVars : ScriptContext
@ -71,6 +69,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
set => SetValue(value); set => SetValue(value);
} }
#pragma warning disable CS0618 // Type or member is obsolete
public ContentData? DataOld public ContentData? DataOld
{ {
get => GetValue<ContentData?>(); get => GetValue<ContentData?>();
@ -90,6 +89,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
SetValue(value); SetValue(value);
} }
} }
#pragma warning restore CS0618 // Type or member is obsolete
[Obsolete("Use dataOld")] [Obsolete("Use dataOld")]
public ContentData? OldData public ContentData? OldData

47
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs

@ -36,9 +36,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
} }
public static IEnumerable<IValidator> CreateValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) public static IEnumerable<IValidator> CreateValidators(ValidatorContext context, IField field, ValidatorFactory factory)
{ {
var args = new Args(context, createFieldValidator); var args = new Args(context, factory);
return field.Accept(Instance, args); return field.Accept(Instance, args);
} }
@ -81,6 +81,34 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
} }
} }
public IEnumerable<IValidator> Visit(IField<ComponentFieldProperties> field, Args args)
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired)
{
yield return new RequiredValidator();
}
yield return ComponentValidator(args.Factory);
}
public IEnumerable<IValidator> Visit(IField<ComponentsFieldProperties> field, Args args)
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired || properties.MinItems != null || properties.MaxItems != null)
{
yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems);
}
yield return new CollectionItemValidator(ComponentValidator(args.Factory));
}
public IEnumerable<IValidator> Visit(IField<DateTimeFieldProperties> field, Args args) public IEnumerable<IValidator> Visit(IField<DateTimeFieldProperties> field, Args args)
{ {
var properties = field.Properties; var properties = field.Properties;
@ -238,5 +266,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return isRequired; return isRequired;
} }
private static IValidator ComponentValidator(ValidatorFactory factory)
{
return new ComponentValidator(schema =>
{
var nestedValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(schema.Fields.Count);
foreach (var nestedField in schema.Fields)
{
nestedValidators[nestedField.Name] = (false, factory(nestedField));
}
return new ObjectValidator<IJsonValue>(nestedValidators, false, "field");
});
}
} }
} }

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

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
public sealed class DefaultValidatorsFactory : IValidatorsFactory public sealed class DefaultValidatorsFactory : IValidatorsFactory
{ {
public IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) public IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory factory)
{ {
if (field is IField<UIFieldProperties>) if (field is IField<UIFieldProperties>)
{ {
@ -21,9 +21,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
} }
} }
public IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) public IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory factory)
{ {
return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, createFieldValidator); return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, factory);
} }
} }
} }

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

@ -14,17 +14,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public interface IValidatorsFactory public interface IValidatorsFactory
{ {
IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory factory)
{ {
yield break; yield break;
} }
IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory factory)
{ {
yield break; yield break;
} }
IEnumerable<IValidator> CreateContentValidators(ValidatorContext context, ValidatorFactory createFieldValidator) IEnumerable<IValidator> CreateContentValidators(ValidatorContext context, ValidatorFactory factory)
{ {
yield break; yield break;
} }

79
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -57,6 +57,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return ConvertToIdList(args.Value); return ConvertToIdList(args.Value);
} }
public (object? Result, JsonError? Error) Visit(IField<ComponentFieldProperties> field, Args args)
{
return ConvertToComponent(field, args.Value);
}
public (object? Result, JsonError? Error) Visit(IField<ComponentsFieldProperties> field, Args args)
{
return ConvertToComponentList(field, args.Value);
}
public (object? Result, JsonError? Error) Visit(IField<ReferencesFieldProperties> field, Args args) public (object? Result, JsonError? Error) Visit(IField<ReferencesFieldProperties> field, Args args)
{ {
return ConvertToIdList(args.Value); return ConvertToIdList(args.Value);
@ -147,22 +157,22 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var result = new List<DomainId>(array.Count); var result = new List<DomainId>(array.Count);
foreach (var item in array) for (var i = 0; i < array.Count; i++)
{ {
if (item is JsonString s && !string.IsNullOrWhiteSpace(s.Value)) if (array[i] is JsonString s && !string.IsNullOrWhiteSpace(s.Value))
{ {
result.Add(DomainId.Create(s.Value)); result.Add(DomainId.Create(s.Value));
} }
else else
{ {
return (null, new JsonError("Invalid json type, expected array of strings.")); return (null, new JsonError(T.Get("contents.invalidArrayOfStrings")));
} }
} }
return (result, null); return (result, null);
} }
return (null, new JsonError("Invalid json type, expected array of strings.")); return (null, new JsonError(T.Get("contents.invalidArrayOfStrings")));
} }
private static (object? Result, JsonError? Error) ConvertToStringList(IJsonValue value) private static (object? Result, JsonError? Error) ConvertToStringList(IJsonValue value)
@ -171,9 +181,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var result = new List<string?>(array.Count); var result = new List<string?>(array.Count);
foreach (var item in array) for (var i = 0; i < array.Count; i++)
{ {
if (item is JsonString s && !string.IsNullOrWhiteSpace(s.Value)) if (array[i] is JsonString s && !string.IsNullOrWhiteSpace(s.Value))
{ {
result.Add(s.Value); result.Add(s.Value);
} }
@ -189,15 +199,42 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); return (null, new JsonError(T.Get("contents.invalidArrayOfStrings")));
} }
private (object? Result, JsonError? Error) ConvertToComponentList(IField<ComponentsFieldProperties> field, IJsonValue value)
{
if (value is JsonArray array)
{
var result = new List<object>(array.Count);
for (var i = 0; i < array.Count; i++)
{
var (item, error) = ConvertToComponent(field, array[i]);
if (error != null)
{
return (null, error);
}
if (item != null)
{
result.Add(item);
}
}
return (result, null);
}
return (null, new JsonError(T.Get("contents.invalidArrayOfObjects")));
}
private static (object? Result, JsonError? Error) ConvertToObjectList(IJsonValue value) private static (object? Result, JsonError? Error) ConvertToObjectList(IJsonValue value)
{ {
if (value is JsonArray array) if (value is JsonArray array)
{ {
var result = new List<JsonObject>(array.Count); var result = new List<JsonObject>(array.Count);
foreach (var item in array) for (var i = 0; i < array.Count; i++)
{ {
if (item is JsonObject obj) if (array[i] is JsonObject obj)
{ {
result.Add(obj); result.Add(obj);
} }
@ -212,5 +249,29 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); return (null, new JsonError(T.Get("contents.invalidArrayOfObjects")));
} }
private static (object? Result, JsonError? Error) ConvertToComponent(IField field, IJsonValue value)
{
if (value is not JsonObject obj)
{
return (null, new JsonError(T.Get("contents.invalidComponentNoObject")));
}
if (!obj.TryGetValue<JsonString>(Component.Discriminator, out var type))
{
return (null, new JsonError(T.Get("contents.invalidComponentNoType")));
}
if (!field.TryGetResolvedSchema(type, out var schema))
{
return (null, new JsonError(T.Get("contents.invalidComponentUnknownSchema")));
}
var data = new JsonObject(obj);
data.Remove(Component.Discriminator);
return (new Component(type.Value, data, schema), null);
}
} }
} }

89
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs

@ -55,34 +55,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return IsValidStringList(args.Value); return IsValidStringList(args.Value);
} }
public bool Visit(IField<ReferencesFieldProperties> field, Args args)
{
return IsValidStringList(args.Value);
}
public bool Visit(IField<TagsFieldProperties> field, Args args)
{
return IsValidStringList(args.Value);
}
public bool Visit(IField<BooleanFieldProperties> field, Args args) public bool Visit(IField<BooleanFieldProperties> field, Args args)
{ {
return args.Value is JsonBoolean; return args.Value is JsonBoolean;
} }
public bool Visit(IField<NumberFieldProperties> field, Args args) public bool Visit(IField<ComponentFieldProperties> field, Args args)
{
return args.Value is JsonNumber;
}
public bool Visit(IField<StringFieldProperties> field, Args args)
{ {
return args.Value is JsonString; return IsValidComponent(args.Value);
} }
public bool Visit(IField<UIFieldProperties> field, Args args) public bool Visit(IField<ComponentsFieldProperties> field, Args args)
{ {
return true; return IsValidComponentList(args.Value);
} }
public bool Visit(IField<DateTimeFieldProperties> field, Args args) public bool Visit(IField<DateTimeFieldProperties> field, Args args)
@ -109,13 +94,38 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return true; return true;
} }
public bool Visit(IField<NumberFieldProperties> field, Args args)
{
return args.Value is JsonNumber;
}
public bool Visit(IField<ReferencesFieldProperties> field, Args args)
{
return IsValidStringList(args.Value);
}
public bool Visit(IField<StringFieldProperties> field, Args args)
{
return args.Value is JsonString;
}
public bool Visit(IField<TagsFieldProperties> field, Args args)
{
return IsValidStringList(args.Value);
}
public bool Visit(IField<UIFieldProperties> field, Args args)
{
return true;
}
private static bool IsValidStringList(IJsonValue value) private static bool IsValidStringList(IJsonValue value)
{ {
if (value is JsonArray array) if (value is JsonArray array)
{ {
foreach (var item in array) for (var i = 0; i < array.Count; i++)
{ {
if (item is not JsonString) if (array[i] is not JsonString)
{ {
return false; return false;
} }
@ -131,9 +141,27 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
if (value is JsonArray array) if (value is JsonArray array)
{ {
foreach (var item in array) for (var i = 0; i < array.Count; i++)
{
if (array[i] is not JsonObject)
{
return false;
}
}
return true;
}
return false;
}
private static bool IsValidComponentList(IJsonValue value)
{
if (value is JsonArray array)
{
for (var i = 0; i < array.Count; i++)
{ {
if (item is not JsonObject) if (!IsValidComponent(array[i]))
{ {
return false; return false;
} }
@ -144,5 +172,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return false; return false;
} }
private static bool IsValidComponent(IJsonValue value)
{
if (value is not JsonObject obj)
{
return false;
}
if (!obj.TryGetValue<JsonString>(Component.Discriminator, out var type))
{
return false;
}
return !string.IsNullOrWhiteSpace(type.Value);
}
} }
} }

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

@ -0,0 +1,37 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class ComponentValidator : IValidator
{
private readonly Func<Schema, IValidator?> validatorFactory;
public ComponentValidator(Func<Schema, IValidator?> validatorFactory)
{
this.validatorFactory = validatorFactory;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is Component component)
{
var validator = validatorFactory(component.Schema);
if (validator != null)
{
await validator.ValidateAsync(component.Data, context, addError);
}
}
}
}
}

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

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -22,6 +22,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
this.fields = fields; this.fields = fields;
this.fieldType = fieldType; this.fieldType = fieldType;
this.isPartial = isPartial; this.isPartial = isPartial;
} }

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

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Caching; using Squidex.Caching;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
@ -16,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Rules.Indexes;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
@ -112,6 +114,8 @@ namespace Squidex.Domain.Apps.Entities
if (schema != null) if (schema != null)
{ {
await ResolveSchemaAsync(appId, schema.SchemaDef);
localCache.Add(cacheKey, schema); localCache.Add(cacheKey, schema);
localCache.Add(SchemaCacheKey(appId, schema.Id), schema); localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
} }
@ -132,6 +136,8 @@ namespace Squidex.Domain.Apps.Entities
if (schema != null) if (schema != null)
{ {
await ResolveSchemaAsync(appId, schema.SchemaDef);
localCache.Add(cacheKey, schema); localCache.Add(cacheKey, schema);
localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema); localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
} }
@ -156,6 +162,17 @@ namespace Squidex.Domain.Apps.Entities
return indexSchemas.GetSchemasAsync(appId); return indexSchemas.GetSchemasAsync(appId);
}); });
foreach (var schema in schemas)
{
localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
}
foreach (var schema in schemas)
{
await ResolveSchemaAsync(appId, schema.SchemaDef);
}
return schemas; return schemas;
} }
@ -176,6 +193,59 @@ namespace Squidex.Domain.Apps.Entities
return rules.Find(x => x.Id == id); return rules.Find(x => x.Id == id);
} }
private async Task ResolveSchemaAsync(DomainId appId, Schema schema)
{
async Task ResolveWithIdsAsync(IField field, ImmutableList<DomainId>? schemaIds)
{
if (schemaIds != null)
{
foreach (var schemaId in schemaIds)
{
if (!field.TryGetResolvedSchema(schemaId, out _))
{
var resolvedEntity = await GetSchemaAsync(appId, schemaId, true);
if (resolvedEntity != null)
{
field.SetResolvedSchema(schemaId, resolvedEntity.SchemaDef);
}
}
}
}
}
async Task ResolveArrayAsync(IArrayField arrayField)
{
foreach (var nestedField in arrayField.Fields)
{
await ResolveAsync(nestedField);
}
}
async Task ResolveAsync(IField field)
{
switch (field)
{
case IField<ComponentFieldProperties> component:
await ResolveWithIdsAsync(field, component.Properties.SchemaIds);
break;
case IField<ComponentsFieldProperties> components:
await ResolveWithIdsAsync(field, components.Properties.SchemaIds);
break;
case IArrayField arrayField:
await ResolveArrayAsync(arrayField);
break;
}
}
foreach (var field in schema.Fields)
{
await ResolveAsync(field);
}
}
private static string AppCacheKey(DomainId appId) private static string AppCacheKey(DomainId appId)
{ {
return $"APPS_ID_{appId}"; return $"APPS_ID_{appId}";

4
backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs

@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
if (value?.StartsWith(Permissions.App, StringComparison.OrdinalIgnoreCase) == true) if (value?.StartsWith(Permissions.App, StringComparison.OrdinalIgnoreCase) == true)
{ {
if (value.IndexOf("{name}", Permissions.App.Length, StringComparison.OrdinalIgnoreCase) >= 0) if (value.IndexOf("{schema}", Permissions.App.Length, StringComparison.OrdinalIgnoreCase) >= 0)
{ {
forAppSchemas.Add(value); forAppSchemas.Add(value);
} }
@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
foreach (var schema in schemaNames) foreach (var schema in schemaNames)
{ {
var replaced = trimmed.Replace("{name}", schema); var replaced = trimmed.Replace("{schema}", schema);
result.Add(replaced); result.Add(replaced);
} }

3
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
await CreateCore(c, operation); await CreateCore(c, operation);
if (operation.Schema.SchemaDef.IsSingleton()) if (operation.Schema.SchemaDef.Type == SchemaType.Singleton)
{ {
ChangeStatus(c.AsChange(Status.Published)); ChangeStatus(c.AsChange(Status.Published));
} }
@ -223,6 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
private async Task CreateCore(CreateContent c, OperationContext operation) private async Task CreateCore(CreateContent c, OperationContext operation)
{ {
operation.MustNotCreateSingleton(); operation.MustNotCreateSingleton();
operation.MustNotCreateForUnpublishedSchema();
operation.MustHaveData(c.Data); operation.MustHaveData(c.Data);
if (!c.DoNotValidate) if (!c.DoNotValidate)

14
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs

@ -14,9 +14,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
public static class SingletonExtensions public static class SingletonExtensions
{ {
public static void MustNotCreateForUnpublishedSchema(this OperationContext context)
{
if (!context.SchemaDef.IsPublished && context.SchemaDef.Type != SchemaType.Singleton)
{
throw new DomainException(T.Get("contents.schemaNotPublished"));
}
}
public static void MustNotCreateSingleton(this OperationContext context) public static void MustNotCreateSingleton(this OperationContext context)
{ {
if (context.SchemaDef.IsSingleton() && context.ContentId != context.Schema.Id) if (context.SchemaDef.Type == SchemaType.Singleton && context.ContentId != context.Schema.Id)
{ {
throw new DomainException(T.Get("contents.singletonNotCreatable")); throw new DomainException(T.Get("contents.singletonNotCreatable"));
} }
@ -24,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
public static void MustNotChangeSingleton(this OperationContext context, Status status) public static void MustNotChangeSingleton(this OperationContext context, Status status)
{ {
if (context.SchemaDef.IsSingleton() && (context.Content.NewStatus == null || status != Status.Published)) if (context.SchemaDef.Type == SchemaType.Singleton && (context.Content.NewStatus == null || status != Status.Published))
{ {
throw new DomainException(T.Get("contents.singletonNotChangeable")); throw new DomainException(T.Get("contents.singletonNotChangeable"));
} }
@ -32,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
public static void MustNotDeleteSingleton(this OperationContext context) public static void MustNotDeleteSingleton(this OperationContext context)
{ {
if (context.SchemaDef.IsSingleton()) if (context.SchemaDef.Type == SchemaType.Singleton)
{ {
throw new DomainException(T.Get("contents.singletonNotDeletable")); throw new DomainException(T.Get("contents.singletonNotDeletable"));
} }

4
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
public static async Task CheckTransitionAsync(this OperationContext context, Status status) public static async Task CheckTransitionAsync(this OperationContext context, Status status)
{ {
if (!context.SchemaDef.IsSingleton()) if (context.SchemaDef.Type != SchemaType.Singleton)
{ {
var workflow = GetWorkflow(context); var workflow = GetWorkflow(context);
@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
public static async Task CheckStatusAsync(this OperationContext context, Status status) public static async Task CheckStatusAsync(this OperationContext context, Status status)
{ {
if (!context.SchemaDef.IsSingleton()) if (context.SchemaDef.Type != SchemaType.Singleton)
{ {
var workflow = GetWorkflow(context); var workflow = GetWorkflow(context);

10
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs

@ -44,6 +44,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
return AllTypes.Boolean; return AllTypes.Boolean;
} }
public IGraphType? Visit(IField<ComponentFieldProperties> field, FieldInfo args)
{
return AllTypes.Json;
}
public IGraphType? Visit(IField<ComponentsFieldProperties> field, FieldInfo args)
{
return AllTypes.Json;
}
public IGraphType? Visit(IField<DateTimeFieldProperties> field, FieldInfo args) public IGraphType? Visit(IField<DateTimeFieldProperties> field, FieldInfo args)
{ {
return AllTypes.DateTime; return AllTypes.DateTime;

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

@ -120,6 +120,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
return (AllTypes.Boolean, JsonBoolean, null); return (AllTypes.Boolean, JsonBoolean, null);
} }
public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField<ComponentFieldProperties> field, FieldInfo args)
{
return (AllTypes.Json, JsonPath, ContentActions.Json.Arguments);
}
public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField<ComponentsFieldProperties> field, FieldInfo args)
{
return (AllTypes.Json, JsonPath, ContentActions.Json.Arguments);
}
public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField<DateTimeFieldProperties> field, FieldInfo args) public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField<DateTimeFieldProperties> field, FieldInfo args)
{ {
return (AllTypes.DateTime, JsonDateTime, null); return (AllTypes.DateTime, JsonDateTime, null);

9
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs

@ -57,7 +57,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{ {
var names = new Names(); var names = new Names();
foreach (var schema in schemas.Where(x => x.SchemaDef.IsPublished && x.SchemaDef.Fields.Count > 0).OrderBy(x => x.Created)) var validSchemas = schemas.Where(x =>
x.SchemaDef.IsPublished &&
x.SchemaDef.Type != SchemaType.Component &&
x.SchemaDef.Fields.Count > 0);
foreach (var schema in validSchemas.OrderBy(x => x.Created))
{ {
var typeName = schema.TypeName(); var typeName = schema.TypeName();
@ -191,4 +196,4 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
return $"{name}{offset + 1}"; return $"{name}{offset + 1}";
} }
} }
} }

9
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -206,7 +206,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var schemas = await appProvider.GetSchemasAsync(context.App.Id); var schemas = await appProvider.GetSchemasAsync(context.App.Id);
return schemas.Where(x => HasPermission(context, x, Permissions.AppContentsReadOwn)).ToList(); return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, Permissions.AppContentsReadOwn)).ToList();
}
private static bool IsAccessible(ISchemaEntity schema)
{
return schema.SchemaDef.IsPublished;
} }
private static bool HasPermission(Context context, ISchemaEntity schema, string permissionId) private static bool HasPermission(Context context, ISchemaEntity schema, string permissionId)

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

@ -43,10 +43,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
this.contentRepository = contentRepository; this.contentRepository = contentRepository;
excludedChangedField = FieldConverters.ExcludeChangedTypes(jsonSerializer); excludedChangedField = FieldConverters.ExcludeChangedTypes(jsonSerializer);
excludedChangedValue = FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeChangedTypes(jsonSerializer))); excludedChangedValue = FieldConverters.ForValues(ValueConverters.ExcludeChangedTypes(jsonSerializer));
excludedHiddenField = FieldConverters.ExcludeHidden; excludedHiddenField = FieldConverters.ExcludeHidden;
excludedHiddenValue = FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeHidden)); excludedHiddenValue = FieldConverters.ForValues(ValueConverters.ExcludeHidden);
} }
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas) public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
@ -125,7 +125,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
if (cleanReferences != null) if (cleanReferences != null)
{ {
yield return FieldConverters.ForValues(cleanReferences); yield return FieldConverters.ForValues(cleanReferences);
yield return FieldConverters.ForValues(ValueConverters.ForNested(cleanReferences));
} }
yield return FieldConverters.ResolveInvariant(context.App.Languages); yield return FieldConverters.ResolveInvariant(context.App.Languages);
@ -154,7 +153,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
var resolveAssetUrls = ValueConverters.ResolveAssetUrls(appId, assetUrls, urlGenerator); var resolveAssetUrls = ValueConverters.ResolveAssetUrls(appId, assetUrls, urlGenerator);
yield return FieldConverters.ForValues(resolveAssetUrls); yield return FieldConverters.ForValues(resolveAssetUrls);
yield return FieldConverters.ForValues(ValueConverters.ForNested(resolveAssetUrls));
} }
} }
} }

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

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
foreach (var content in group) foreach (var content in group)
{ {
content.IsSingleton = schema.SchemaDef.IsSingleton(); content.IsSingleton = schema.SchemaDef.Type == SchemaType.Singleton;
content.SchemaName = schemaName; content.SchemaName = schemaName;
content.SchemaDisplayName = schemaDisplayName; content.SchemaDisplayName = schemaDisplayName;

17
backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/FieldPropertiesValidator.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -91,6 +91,21 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards
} }
} }
public IEnumerable<ValidationError> Visit(ComponentFieldProperties properties, None args)
{
yield break;
}
public IEnumerable<ValidationError> Visit(ComponentsFieldProperties properties, None args)
{
if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems))
{
yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)),
nameof(properties.MinItems),
nameof(properties.MaxItems));
}
}
public IEnumerable<ValidationError> Visit(DateTimeFieldProperties properties, None args) public IEnumerable<ValidationError> Visit(DateTimeFieldProperties properties, None args)
{ {
if (!properties.Editor.IsEnumValue()) if (!properties.Editor.IsEnumValue())

4
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs

@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
AddSchemaUrl(result, appId, schemaId, name); AddSchemaUrl(result, appId, schemaId, name);
if (HasPermission(context, schemaId)) if (schema.SchemaDef.Type != SchemaType.Component && HasPermission(context, schemaId))
{ {
AddContentsUrl(result, appId, schema, schemaId, name); AddContentsUrl(result, appId, schema, schemaId, name);
} }
@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
private void AddContentsUrl(SearchResults result, NamedId<DomainId> appId, ISchemaEntity schema, NamedId<DomainId> schemaId, string name) private void AddContentsUrl(SearchResults result, NamedId<DomainId> appId, ISchemaEntity schema, NamedId<DomainId> schemaId, string name)
{ {
if (schema.SchemaDef.IsSingleton()) if (schema.SchemaDef.Type == SchemaType.Singleton)
{ {
var contentUrl = urlGenerator.ContentUI(appId, schemaId, schemaId.Id); var contentUrl = urlGenerator.ContentUI(appId, schemaId, schemaId.Id);

1
backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs

@ -9,7 +9,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using Squidex.Infrastructure.Collections;
namespace Squidex.Infrastructure.Json.Newtonsoft namespace Squidex.Infrastructure.Json.Newtonsoft
{ {

40
backend/src/Squidex.Shared/Permissions.cs

@ -138,33 +138,33 @@ namespace Squidex.Shared
public const string AppSchemas = "squidex.apps.{app}.schemas"; public const string AppSchemas = "squidex.apps.{app}.schemas";
public const string AppSchemasRead = "squidex.apps.{app}.schemas.read"; public const string AppSchemasRead = "squidex.apps.{app}.schemas.read";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create"; public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create";
public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{schema}.update";
public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{schema}.scripts";
public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish"; public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{schema}.publish";
public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete"; public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{schema}.delete";
// Contents // Contents
public const string AppContents = "squidex.apps.{app}.contents.{name}"; public const string AppContents = "squidex.apps.{app}.contents.{schema}";
public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; public const string AppContentsRead = "squidex.apps.{app}.contents.{schema}.read";
public const string AppContentsReadOwn = "squidex.apps.{app}.contents.{name}.read.own"; public const string AppContentsReadOwn = "squidex.apps.{app}.contents.{schema}.read.own";
public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; public const string AppContentsCreate = "squidex.apps.{app}.contents.{schema}.create";
public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; public const string AppContentsUpdate = "squidex.apps.{app}.contents.{schema}.update";
public const string AppContentsUpdateOwn = "squidex.apps.{app}.contents.{name}.update.own"; public const string AppContentsUpdateOwn = "squidex.apps.{app}.contents.{schema}.update.own";
public const string AppContentsChangeStatus = "squidex.apps.{app}.contents.{name}.changestatus"; public const string AppContentsChangeStatus = "squidex.apps.{app}.contents.{schema}.changestatus";
public const string AppContentsChangeStatusOwn = "squidex.apps.{app}.contents.{name}.changestatus.own"; public const string AppContentsChangeStatusOwn = "squidex.apps.{app}.contents.{schema}.changestatus.own";
public const string AppContentsUpsert = "squidex.apps.{app}.contents.{name}.upsert"; public const string AppContentsUpsert = "squidex.apps.{app}.contents.{schema}.upsert";
public const string AppContentsVersionCreate = "squidex.apps.{app}.contents.{name}.version.create"; public const string AppContentsVersionCreate = "squidex.apps.{app}.contents.{schema}.version.create";
public const string AppContentsVersionCreateOwn = "squidex.apps.{app}.contents.{name}.version.create.own"; public const string AppContentsVersionCreateOwn = "squidex.apps.{app}.contents.{schema}.version.create.own";
public const string AppContentsVersionDelete = "squidex.apps.{app}.contents.{name}.version.delete"; public const string AppContentsVersionDelete = "squidex.apps.{app}.contents.{schema}.version.delete";
public const string AppContentsVersionDeleteOwn = "squidex.apps.{app}.contents.{name}.version.delete.own"; public const string AppContentsVersionDeleteOwn = "squidex.apps.{app}.contents.{schema}.version.delete.own";
public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; public const string AppContentsDelete = "squidex.apps.{app}.contents.{schema}.delete";
public const string AppContentsDeleteOwn = "squidex.apps.{app}.contents.{name}.delete.own"; public const string AppContentsDeleteOwn = "squidex.apps.{app}.contents.{schema}.delete.own";
public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any)
{ {
Guard.NotNull(id, nameof(id)); Guard.NotNull(id, nameof(id));
return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any)); return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{schema}", schema ?? Permission.Any));
} }
} }
} }

12
backend/src/Squidex.Shared/Texts.it.resx

@ -460,6 +460,15 @@
<data name="contents.invalidBoolean" xml:space="preserve"> <data name="contents.invalidBoolean" xml:space="preserve">
<value>Errore nel json, atteso un boolean.</value> <value>Errore nel json, atteso un boolean.</value>
</data> </data>
<data name="contents.invalidComponentNoObject" xml:space="preserve">
<value>Invalid json object, expected object with 'schemaId' field.</value>
</data>
<data name="contents.invalidComponentNoType" xml:space="preserve">
<value>Invalid component. No 'schemaId' field found.</value>
</data>
<data name="contents.invalidComponentUnknownSchema" xml:space="preserve">
<value>Invalid component. Cannot find schema.</value>
</data>
<data name="contents.invalidGeolocation" xml:space="preserve"> <data name="contents.invalidGeolocation" xml:space="preserve">
<value>Errore nel json, atteso un object latitudine/longitudine.</value> <value>Errore nel json, atteso un object latitudine/longitudine.</value>
</data> </data>
@ -481,6 +490,9 @@
<data name="contents.referenced" xml:space="preserve"> <data name="contents.referenced" xml:space="preserve">
<value>Il contenuto è collegato ad un altro contenuto e pertanto non può essere cancellato.</value> <value>Il contenuto è collegato ad un altro contenuto e pertanto non può essere cancellato.</value>
</data> </data>
<data name="contents.schemaNotPublished" xml:space="preserve">
<value>Schema not published.</value>
</data>
<data name="contents.singletonNotChangeable" xml:space="preserve"> <data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Il contenuto singleton non può essere aggiornato</value> <value>Il contenuto singleton non può essere aggiornato</value>
</data> </data>

12
backend/src/Squidex.Shared/Texts.nl.resx

@ -460,6 +460,15 @@
<data name="contents.invalidBoolean" xml:space="preserve"> <data name="contents.invalidBoolean" xml:space="preserve">
<value>Ongeldig json-type, verwachte boolean.</value> <value>Ongeldig json-type, verwachte boolean.</value>
</data> </data>
<data name="contents.invalidComponentNoObject" xml:space="preserve">
<value>Invalid json object, expected object with 'schemaId' field.</value>
</data>
<data name="contents.invalidComponentNoType" xml:space="preserve">
<value>Invalid component. No 'schemaId' field found.</value>
</data>
<data name="contents.invalidComponentUnknownSchema" xml:space="preserve">
<value>Invalid component. Cannot find schema.</value>
</data>
<data name="contents.invalidGeolocation" xml:space="preserve"> <data name="contents.invalidGeolocation" xml:space="preserve">
<value>Ongeldig json-type, verwacht object voor lengte- / breedtegraad.</value> <value>Ongeldig json-type, verwacht object voor lengte- / breedtegraad.</value>
</data> </data>
@ -481,6 +490,9 @@
<data name="contents.referenced" xml:space="preserve"> <data name="contents.referenced" xml:space="preserve">
<value>Er wordt naar de inhoud verwezen door een andere item en kan niet worden verwijderd.</value> <value>Er wordt naar de inhoud verwezen door een andere item en kan niet worden verwijderd.</value>
</data> </data>
<data name="contents.schemaNotPublished" xml:space="preserve">
<value>Schema not published.</value>
</data>
<data name="contents.singletonNotChangeable" xml:space="preserve"> <data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Singleton-inhoud kan niet worden bijgewerkt.</value> <value>Singleton-inhoud kan niet worden bijgewerkt.</value>
</data> </data>

12
backend/src/Squidex.Shared/Texts.resx

@ -460,6 +460,15 @@
<data name="contents.invalidBoolean" xml:space="preserve"> <data name="contents.invalidBoolean" xml:space="preserve">
<value>Invalid json type, expected boolean.</value> <value>Invalid json type, expected boolean.</value>
</data> </data>
<data name="contents.invalidComponentNoObject" xml:space="preserve">
<value>Invalid json object, expected object with 'schemaId' field.</value>
</data>
<data name="contents.invalidComponentNoType" xml:space="preserve">
<value>Invalid component. No 'schemaId' field found.</value>
</data>
<data name="contents.invalidComponentUnknownSchema" xml:space="preserve">
<value>Invalid component. Cannot find schema.</value>
</data>
<data name="contents.invalidGeolocation" xml:space="preserve"> <data name="contents.invalidGeolocation" xml:space="preserve">
<value>Invalid json type, expected latitude/longitude object.</value> <value>Invalid json type, expected latitude/longitude object.</value>
</data> </data>
@ -481,6 +490,9 @@
<data name="contents.referenced" xml:space="preserve"> <data name="contents.referenced" xml:space="preserve">
<value>Content is referenced by another content and cannot be deleted or unpublished.</value> <value>Content is referenced by another content and cannot be deleted or unpublished.</value>
</data> </data>
<data name="contents.schemaNotPublished" xml:space="preserve">
<value>Schema not published.</value>
</data>
<data name="contents.singletonNotChangeable" xml:space="preserve"> <data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Singleton content cannot be updated.</value> <value>Singleton content cannot be updated.</value>
</data> </data>

18
backend/src/Squidex.Web/ApiController.cs

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -29,7 +30,7 @@ namespace Squidex.Web
{ {
get get
{ {
var app = HttpContext.Context().App; var app = HttpContext.Features.Get<IAppFeature>()?.App;
if (app == null) if (app == null)
{ {
@ -40,6 +41,21 @@ namespace Squidex.Web
} }
} }
protected ISchemaEntity Schema
{
get
{
var schema = HttpContext.Features.Get<ISchemaFeature>()?.Schema;
if (schema == null)
{
throw new InvalidOperationException("Not in a schema context.");
}
return schema;
}
}
protected Resources Resources protected Resources Resources
{ {
get => resources.Value; get => resources.Value;

4
backend/src/Squidex.Web/ApiPermissionAttribute.cs

@ -43,14 +43,14 @@ namespace Squidex.Web
{ {
foreach (var id in permissionIds) foreach (var id in permissionIds)
{ {
var app = context.HttpContext.Features.Get<IAppFeature>()?.AppId.Name; var app = context.HttpContext.Features.Get<IAppFeature>()?.App.Name;
if (string.IsNullOrWhiteSpace(app)) if (string.IsNullOrWhiteSpace(app))
{ {
app = Permission.Any; app = Permission.Any;
} }
var schema = context.HttpContext.Features.Get<ISchemaFeature>()?.SchemaId.Name; var schema = context.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name;
if (string.IsNullOrWhiteSpace(schema)) if (string.IsNullOrWhiteSpace(schema))
{ {

5
backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs

@ -9,6 +9,7 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -51,7 +52,7 @@ namespace Squidex.Web.CommandMiddlewares
throw new InvalidOperationException("Cannot resolve schema."); throw new InvalidOperationException("Cannot resolve schema.");
} }
return feature.SchemaId; return feature.Schema.NamedId();
} }
} }
} }

4
backend/src/Squidex.Web/IAppFeature.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure; using Squidex.Domain.Apps.Entities.Apps;
namespace Squidex.Web namespace Squidex.Web
{ {
public interface IAppFeature public interface IAppFeature
{ {
NamedId<DomainId> AppId { get; } IAppEntity App { get; }
} }
} }

4
backend/src/Squidex.Web/ISchemaFeature.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure; using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Web namespace Squidex.Web
{ {
public interface ISchemaFeature public interface ISchemaFeature
{ {
NamedId<DomainId> SchemaId { get; } ISchemaEntity Schema { get; }
} }
} }

14
backend/src/Squidex.Web/Pipeline/AppFeature.cs

@ -5,17 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure; using Squidex.Domain.Apps.Entities.Apps;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
public sealed class AppFeature : IAppFeature public sealed record AppFeature(IAppEntity App) : IAppFeature;
{
public NamedId<DomainId> AppId { get; }
public AppFeature(NamedId<DomainId> appId)
{
AppId = appId;
}
}
} }

2
backend/src/Squidex.Web/Pipeline/AppResolver.cs

@ -110,7 +110,7 @@ namespace Squidex.Web.Pipeline
return; return;
} }
context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app.NamedId())); context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app));
context.HttpContext.Response.Headers.Add("X-AppId", app.Id.ToString()); context.HttpContext.Response.Headers.Add("X-AppId", app.Id.ToString());
} }
else else

14
backend/src/Squidex.Web/Pipeline/SchemaFeature.cs

@ -5,17 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure; using Squidex.Domain.Apps.Entities.Schemas;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
public sealed class SchemaFeature : ISchemaFeature public sealed record SchemaFeature(ISchemaEntity Schema) : ISchemaFeature;
{
public NamedId<DomainId> SchemaId { get; }
public SchemaFeature(NamedId<DomainId> schemaId)
{
SchemaId = schemaId;
}
}
} }

13
backend/src/Squidex.Web/Pipeline/SchemaResolver.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -31,11 +32,11 @@ namespace Squidex.Web.Pipeline
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{ {
var appId = context.HttpContext.Features.Get<IAppFeature>()?.AppId.Id ?? default; var appId = context.HttpContext.Features.Get<IAppFeature>()?.App.Id ?? default;
if (appId != default) if (appId != default)
{ {
var schemaIdOrName = context.RouteData.Values["name"]?.ToString(); var schemaIdOrName = context.RouteData.Values["schema"]?.ToString();
if (!string.IsNullOrWhiteSpace(schemaIdOrName)) if (!string.IsNullOrWhiteSpace(schemaIdOrName))
{ {
@ -47,7 +48,13 @@ namespace Squidex.Web.Pipeline
return; return;
} }
context.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(schema.NamedId())); if (context.ActionDescriptor.EndpointMetadata.Any(x => x is SchemaMustBePublishedAttribute) && !schema.SchemaDef.IsPublished)
{
context.Result = new NotFoundResult();
return;
}
context.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(schema));
} }
} }

8
backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs

@ -48,7 +48,7 @@ namespace Squidex.Web.Pipeline
{ {
if (context.Response.StatusCode != StatusCodes.Status429TooManyRequests) if (context.Response.StatusCode != StatusCodes.Status429TooManyRequests)
{ {
var appId = context.Features.Get<IAppFeature>()?.AppId; var appId = context.Features.Get<IAppFeature>()?.App.Id;
if (appId != null) if (appId != null)
{ {
@ -72,13 +72,13 @@ namespace Squidex.Web.Pipeline
request.UserClientId = clientId; request.UserClientId = clientId;
request.UserId = context.User.OpenIdSubject(); request.UserId = context.User.OpenIdSubject();
await usageLog.LogAsync(appId.Id, request); await usageLog.LogAsync(appId.Value, request);
if (request.Costs > 0) if (request.Costs > 0)
{ {
var date = request.Timestamp.ToDateTimeUtc().Date; var date = request.Timestamp.ToDateTimeUtc().Date;
await usageTracker.TrackAsync(date, appId.Id.ToString(), await usageTracker.TrackAsync(date, appId.Value.ToString(),
request.UserClientId, request.UserClientId,
request.Costs, request.Costs,
request.ElapsedMs, request.ElapsedMs,
@ -100,4 +100,4 @@ namespace Squidex.Web.Pipeline
return usageBody; return usageBody;
} }
} }
} }

2
backend/src/Squidex.Web/Resources.cs

@ -213,7 +213,7 @@ namespace Squidex.Web
if (schema == Permission.Any) if (schema == Permission.Any)
{ {
var falback = Controller.HttpContext.Features.Get<ISchemaFeature>()?.SchemaId.Name; var falback = Controller.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name;
if (!string.IsNullOrWhiteSpace(falback)) if (!string.IsNullOrWhiteSpace(falback))
{ {

16
backend/src/Squidex.Web/SchemaMustBePublishedAttribute.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Web
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class SchemaMustBePublishedAttribute : Attribute
{
}
}

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

@ -120,6 +120,8 @@ namespace Squidex.Areas.Api.Config.OpenApi
CreateObjectMap<AssetMetadata>() CreateObjectMap<AssetMetadata>()
}; };
settings.SchemaType = SchemaType.OpenApi3;
settings.FlattenInheritanceHierarchy = flatten; settings.FlattenInheritanceHierarchy = flatten;
} }

8
backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Linq;
using NJsonSchema; using NJsonSchema;
using NSwag; using NSwag;
@ -26,6 +27,11 @@ namespace Squidex.Areas.Api.Config.OpenApi
void AddQuery(OpenApiParameter parameter) void AddQuery(OpenApiParameter parameter)
{ {
if (operation.Parameters.Any(x => x.Name == parameter.Name && x.Kind == OpenApiParameterKind.Query))
{
return;
}
parameter.Kind = OpenApiParameterKind.Query; parameter.Kind = OpenApiParameterKind.Query;
operation.Parameters.Add(parameter); operation.Parameters.Add(parameter);
@ -84,4 +90,4 @@ namespace Squidex.Areas.Api.Config.OpenApi
}); });
} }
} }
} }

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

@ -23,6 +23,7 @@ using Squidex.Web.GraphQL;
namespace Squidex.Areas.Api.Controllers.Contents namespace Squidex.Areas.Api.Controllers.Contents
{ {
[SchemaMustBePublishedAttribute]
public sealed class ContentsController : ApiController public sealed class ContentsController : ApiController
{ {
private readonly IContentQueryService contentQuery; private readonly IContentQueryService contentQuery;
@ -125,7 +126,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Queries contents. /// Queries contents.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="ids">The optional ids of the content to fetch.</param> /// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="q">The optional json query.</param> /// <param name="q">The optional json query.</param>
/// <returns> /// <returns>
@ -136,19 +137,17 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/")] [Route("content/{app}/{schema}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string? ids = null, [FromQuery] string? q = null) public async Task<IActionResult> GetContents(string app, string schema, [FromQuery] string? ids = null, [FromQuery] string? q = null)
{ {
var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); var contents = await contentQuery.QueryAsync(Context, schema, CreateQuery(ids, q));
var contents = await contentQuery.QueryAsync(Context, name, CreateQuery(ids, q));
var response = Deferred.AsyncResponse(() => var response = Deferred.AsyncResponse(() =>
{ {
return ContentsDto.FromContentsAsync(contents, Resources, schema, contentWorkflow); return ContentsDto.FromContentsAsync(contents, Resources, Schema, contentWorkflow);
}); });
return Ok(response); return Ok(response);
@ -158,7 +157,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Queries contents. /// Queries contents.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="query">The required query object.</param> /// <param name="query">The required query object.</param>
/// <returns> /// <returns>
/// 200 => Contents returned. /// 200 => Contents returned.
@ -168,19 +167,17 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/query")] [Route("content/{app}/{schema}/query")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContentsPost(string app, string name, [FromBody] QueryDto query) public async Task<IActionResult> GetContentsPost(string app, string schema, [FromBody] QueryDto query)
{ {
var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); var contents = await contentQuery.QueryAsync(Context, schema, query?.ToQuery() ?? Q.Empty);
var contents = await contentQuery.QueryAsync(Context, name, query?.ToQuery() ?? Q.Empty);
var response = Deferred.AsyncResponse(() => var response = Deferred.AsyncResponse(() =>
{ {
return ContentsDto.FromContentsAsync(contents, Resources, schema, contentWorkflow); return ContentsDto.FromContentsAsync(contents, Resources, Schema, contentWorkflow);
}); });
return Ok(response); return Ok(response);
@ -190,7 +187,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Get a content item. /// Get a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param> /// <param name="id">The id of the content to fetch.</param>
/// <returns> /// <returns>
/// 200 => Content returned. /// 200 => Content returned.
@ -200,13 +197,13 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, DomainId id) public async Task<IActionResult> GetContent(string app, string schema, DomainId id)
{ {
var content = await contentQuery.FindAsync(Context, name, id); var content = await contentQuery.FindAsync(Context, schema, id);
if (content == null) if (content == null)
{ {
@ -222,7 +219,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Get a content item validity. /// Get a content item validity.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param> /// <param name="id">The id of the content to fetch.</param>
/// <returns> /// <returns>
/// 204 => Content is valid. /// 204 => Content is valid.
@ -233,10 +230,10 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/{id}/validity")] [Route("content/{app}/{schema}/{id}/validity")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContentValidity(string app, string name, DomainId id) public async Task<IActionResult> GetContentValidity(string app, string schema, DomainId id)
{ {
var command = new ValidateContent { ContentId = id }; var command = new ValidateContent { ContentId = id };
@ -249,7 +246,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Get all references of a content. /// Get all references of a content.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param> /// <param name="id">The id of the content to fetch.</param>
/// <param name="q">The optional json query.</param> /// <param name="q">The optional json query.</param>
/// <returns> /// <returns>
@ -260,11 +257,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/{id}/references")] [Route("content/{app}/{schema}/{id}/references")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetReferences(string app, string name, DomainId id, [FromQuery] string? q = null) public async Task<IActionResult> GetReferences(string app, string schema, DomainId id, [FromQuery] string? q = null)
{ {
var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id)); var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id));
@ -280,7 +277,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Get a referencing contents of a content item. /// Get a referencing contents of a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param> /// <param name="id">The id of the content to fetch.</param>
/// <param name="q">The optional json query.</param> /// <param name="q">The optional json query.</param>
/// <returns> /// <returns>
@ -291,11 +288,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/{id}/referencing")] [Route("content/{app}/{schema}/{id}/referencing")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetReferencing(string app, string name, DomainId id, [FromQuery] string? q = null) public async Task<IActionResult> GetReferencing(string app, string schema, DomainId id, [FromQuery] string? q = null)
{ {
var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id)); var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id));
@ -311,7 +308,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Get a content by version. /// Get a content by version.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content to fetch.</param> /// <param name="id">The id of the content to fetch.</param>
/// <param name="version">The version fo the content to fetch.</param> /// <param name="version">The version fo the content to fetch.</param>
/// <returns> /// <returns>
@ -322,12 +319,12 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/{id}/{version}/")] [Route("content/{app}/{schema}/{id}/{version}/")]
[ApiPermissionOrAnonymous(Permissions.AppContentsReadOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsReadOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string app, string name, DomainId id, int version) public async Task<IActionResult> GetContentVersion(string app, string schema, DomainId id, int version)
{ {
var content = await contentQuery.FindAsync(Context, name, id, version); var content = await contentQuery.FindAsync(Context, schema, id, version);
if (content == null) if (content == null)
{ {
@ -343,7 +340,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Create a content item. /// Create a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <returns>
/// 201 => Content created. /// 201 => Content created.
@ -354,24 +351,24 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/")] [Route("content/{app}/{schema}/")]
[ProducesResponseType(typeof(ContentsDto), 201)] [ProducesResponseType(typeof(ContentsDto), 201)]
[ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, CreateContentDto request) public async Task<IActionResult> PostContent(string app, string schema, CreateContentDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetContent), new { app, name, id = command.ContentId }, response); return CreatedAtAction(nameof(GetContent), new { app, schema, id = command.ContentId }, response);
} }
/// <summary> /// <summary>
/// Import content items. /// Import content items.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The import request.</param> /// <param name="request">The import request.</param>
/// <returns> /// <returns>
/// 200 => Contents created. /// 200 => Contents created.
@ -382,12 +379,12 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/import")] [Route("content/{app}/{schema}/import")]
[ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)]
[ApiCosts(5)] [ApiCosts(5)]
[Obsolete("Use bulk endpoint now.")] [Obsolete("Use bulk endpoint now.")]
public async Task<IActionResult> PostContents(string app, string name, [FromBody] ImportContentsDto request) public async Task<IActionResult> PostContents(string app, string schema, [FromBody] ImportContentsDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -403,7 +400,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Bulk update content items. /// Bulk update content items.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The bulk update request.</param> /// <param name="request">The bulk update request.</param>
/// <returns> /// <returns>
/// 201 => Contents created, update or delete. /// 201 => Contents created, update or delete.
@ -414,11 +411,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/bulk")] [Route("content/{app}/{schema}/bulk")]
[ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsReadOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsReadOwn)]
[ApiCosts(5)] [ApiCosts(5)]
public async Task<IActionResult> BulkUpdateContents(string app, string name, [FromBody] BulkUpdateContentsDto request) public async Task<IActionResult> BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -434,7 +431,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Upsert a content item. /// Upsert a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content item to update.</param> /// <param name="id">The id of the content item to update.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <returns>
@ -446,11 +443,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsUpsert)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpsert)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostUpsertContent(string app, string name, DomainId id, UpsertContentDto request) public async Task<IActionResult> PostUpsertContent(string app, string schema, DomainId id, UpsertContentDto request)
{ {
var command = request.ToCommand(id); var command = request.ToCommand(id);
@ -463,7 +460,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Update a content item. /// Update a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content item to update.</param> /// <param name="id">The id of the content item to update.</param>
/// <param name="request">The full data for the content item.</param> /// <param name="request">The full data for the content item.</param>
/// <returns> /// <returns>
@ -475,11 +472,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsUpdateOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdateOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, DomainId id, [FromBody] ContentData request) public async Task<IActionResult> PutContent(string app, string schema, DomainId id, [FromBody] ContentData request)
{ {
var command = new UpdateContent { ContentId = id, Data = request }; var command = new UpdateContent { ContentId = id, Data = request };
@ -492,7 +489,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Patchs a content item. /// Patchs a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content item to patch.</param> /// <param name="id">The id of the content item to patch.</param>
/// <param name="request">The patch for the content item.</param> /// <param name="request">The patch for the content item.</param>
/// <returns> /// <returns>
@ -504,11 +501,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPatch] [HttpPatch]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsUpdateOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdateOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, DomainId id, [FromBody] ContentData request) public async Task<IActionResult> PatchContent(string app, string schema, DomainId id, [FromBody] ContentData request)
{ {
var command = new PatchContent { ContentId = id, Data = request }; var command = new PatchContent { ContentId = id, Data = request };
@ -521,7 +518,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Change status of a content item. /// Change status of a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content item to change.</param> /// <param name="id">The id of the content item to change.</param>
/// <param name="request">The status request.</param> /// <param name="request">The status request.</param>
/// <returns> /// <returns>
@ -533,11 +530,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("content/{app}/{name}/{id}/status/")] [Route("content/{app}/{schema}/{id}/status/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsChangeStatusOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsChangeStatusOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string name, DomainId id, [FromBody] ChangeStatusDto request) public async Task<IActionResult> PutContentStatus(string app, string schema, DomainId id, [FromBody] ChangeStatusDto request)
{ {
var command = request.ToCommand(id); var command = request.ToCommand(id);
@ -550,7 +547,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Create a new draft version. /// Create a new draft version.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content item to create the draft for.</param> /// <param name="id">The id of the content item to create the draft for.</param>
/// <returns> /// <returns>
/// 200 => Content draft created. /// 200 => Content draft created.
@ -560,11 +557,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/{id}/draft/")] [Route("content/{app}/{schema}/{id}/draft/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsVersionCreateOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsVersionCreateOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> CreateDraft(string app, string name, DomainId id) public async Task<IActionResult> CreateDraft(string app, string schema, DomainId id)
{ {
var command = new CreateContentDraft { ContentId = id }; var command = new CreateContentDraft { ContentId = id };
@ -577,7 +574,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Delete the draft version. /// Delete the draft version.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content item to delete the draft from.</param> /// <param name="id">The id of the content item to delete the draft from.</param>
/// <returns> /// <returns>
/// 200 => Content draft deleted. /// 200 => Content draft deleted.
@ -587,11 +584,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpDelete] [HttpDelete]
[Route("content/{app}/{name}/{id}/draft/")] [Route("content/{app}/{schema}/{id}/draft/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteVersion(string app, string name, DomainId id) public async Task<IActionResult> DeleteVersion(string app, string schema, DomainId id)
{ {
var command = new DeleteContentDraft { ContentId = id }; var command = new DeleteContentDraft { ContentId = id };
@ -604,7 +601,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Delete a content item. /// Delete a content item.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the content item to delete.</param> /// <param name="id">The id of the content item to delete.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <returns>
@ -616,10 +613,10 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// You can create an generated documentation for your app at /api/content/{appName}/docs. /// You can create an generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpDelete] [HttpDelete]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{schema}/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)] [ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, DomainId id, DeleteContentDto request) public async Task<IActionResult> DeleteContent(string app, string schema, DomainId id, DeleteContentDto request)
{ {
var command = request.ToCommand(id); var command = request.ToCommand(id);

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

@ -31,9 +31,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
public JsonSchema DataSchema { get; init; } public JsonSchema DataSchema { get; init; }
public string FormatText(string text) public string? FormatText(string text)
{ {
return text?.Replace("schema ", $"'{SchemaDisplayName}' ", StringComparison.OrdinalIgnoreCase)!; return text?.Replace("[schema]", $"'{SchemaDisplayName}'", StringComparison.Ordinal);
} }
public OperationBuilder AddOperation(string method, string path) public OperationBuilder AddOperation(string method, string path)

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

@ -19,6 +19,7 @@ using Squidex.Hosting;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Properties; using Squidex.Properties;
using Squidex.Shared; using Squidex.Shared;
using SchemaDefType = Squidex.Domain.Apps.Core.Schemas.SchemaType;
namespace Squidex.Areas.Api.Controllers.Contents.Generator namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
@ -49,16 +50,24 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
requestCache.AddDependency(app.UniqueId, app.Version); requestCache.AddDependency(app.UniqueId, app.Version);
foreach (var schema in schemas)
{
requestCache.AddDependency(schema.UniqueId, schema.Version);
}
var builder = new Builder( var builder = new Builder(
app, app,
document, document,
schemaResolver, schemaResolver,
schemaGenerator); schemaGenerator);
foreach (var schema in schemas.Where(x => x.SchemaDef.IsPublished)) var validSchemas = schemas.Where(x =>
{ x.SchemaDef.IsPublished &&
requestCache.AddDependency(schema.UniqueId, schema.Version); x.SchemaDef.Type != SchemaDefType.Component &&
x.SchemaDef.Fields.Count > 0);
foreach (var schema in validSchemas)
{
GenerateSchemaOperations(builder.Schema(schema.SchemaDef, flat)); GenerateSchemaOperations(builder.Schema(schema.SchemaDef, flat));
} }
@ -96,7 +105,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Get, "/") builder.AddOperation(OpenApiOperationMethod.Get, "/")
.RequirePermission(Permissions.AppContentsReadOwn) .RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Query") .Operation("Query")
.OperationSummary("Query schema contents items.") .OperationSummary("Query [schema] contents items.")
.Describe(Resources.OpenApiSchemaQuery) .Describe(Resources.OpenApiSchemaQuery)
.HasQueryOptions(true) .HasQueryOptions(true)
.Responds(200, "Content items retrieved.", builder.ContentsSchema) .Responds(200, "Content items retrieved.", builder.ContentsSchema)
@ -105,14 +114,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Get, "/{id}") builder.AddOperation(OpenApiOperationMethod.Get, "/{id}")
.RequirePermission(Permissions.AppContentsReadOwn) .RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Get") .Operation("Get")
.OperationSummary("Get a schema content item.") .OperationSummary("Get a [schema] content item.")
.HasId() .HasId()
.Responds(200, "Content item returned.", builder.ContentSchema); .Responds(200, "Content item returned.", builder.ContentSchema);
builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/{version}") builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/{version}")
.RequirePermission(Permissions.AppContentsReadOwn) .RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Get") .Operation("Get")
.OperationSummary("Get a schema content item by id and version.") .OperationSummary("Get a [schema] content item by id and version.")
.HasPath("version", JsonObjectType.Number, "The version of the content item.") .HasPath("version", JsonObjectType.Number, "The version of the content item.")
.HasId() .HasId()
.Responds(200, "Content item returned.", builder.ContentSchema); .Responds(200, "Content item returned.", builder.ContentSchema);
@ -120,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/validity") builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/validity")
.RequirePermission(Permissions.AppContentsReadOwn) .RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Validate") .Operation("Validate")
.OperationSummary("Validates a schema content item.") .OperationSummary("Validates a [schema] content item.")
.HasId() .HasId()
.Responds(200, "Content item is valid.") .Responds(200, "Content item is valid.")
.Responds(400, "Content item is not valid."); .Responds(400, "Content item is not valid.");
@ -128,7 +137,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Post, "/") builder.AddOperation(OpenApiOperationMethod.Post, "/")
.RequirePermission(Permissions.AppContentsCreate) .RequirePermission(Permissions.AppContentsCreate)
.Operation("Create") .Operation("Create")
.OperationSummary("Create a schema content item.") .OperationSummary("Create a [schema] content item.")
.HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.") .HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.")
.HasQuery("id", JsonObjectType.String, "The optional custom content id.") .HasQuery("id", JsonObjectType.String, "The optional custom content id.")
.HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody)
@ -138,7 +147,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Post, "/{id}") builder.AddOperation(OpenApiOperationMethod.Post, "/{id}")
.RequirePermission(Permissions.AppContentsUpsert) .RequirePermission(Permissions.AppContentsUpsert)
.Operation("Upsert") .Operation("Upsert")
.OperationSummary("Upsert a schema content item.") .OperationSummary("Upsert a [schema] content item.")
.HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.") .HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.")
.HasId() .HasId()
.HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody)
@ -148,7 +157,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Put, "/{id}") builder.AddOperation(OpenApiOperationMethod.Put, "/{id}")
.RequirePermission(Permissions.AppContentsUpdateOwn) .RequirePermission(Permissions.AppContentsUpdateOwn)
.Operation("Update") .Operation("Update")
.OperationSummary("Update a schema content item.") .OperationSummary("Update a [schema] content item.")
.HasId() .HasId()
.HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody)
.Responds(200, "Content item updated.", builder.ContentSchema) .Responds(200, "Content item updated.", builder.ContentSchema)
@ -157,7 +166,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Patch, "/{id}") builder.AddOperation(OpenApiOperationMethod.Patch, "/{id}")
.RequirePermission(Permissions.AppContentsUpdateOwn) .RequirePermission(Permissions.AppContentsUpdateOwn)
.Operation("Patch") .Operation("Patch")
.OperationSummary("Patch a schema content item.") .OperationSummary("Patch a [schema] content item.")
.HasId() .HasId()
.HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody)
.Responds(200, "Content item updated.", builder.ContentSchema) .Responds(200, "Content item updated.", builder.ContentSchema)
@ -166,7 +175,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Put, "/{id}/status") builder.AddOperation(OpenApiOperationMethod.Put, "/{id}/status")
.RequirePermission(Permissions.AppContentsChangeStatusOwn) .RequirePermission(Permissions.AppContentsChangeStatusOwn)
.Operation("Change") .Operation("Change")
.OperationSummary("Change the status of a schema content item.") .OperationSummary("Change the status of a [schema] content item.")
.HasId() .HasId()
.HasBody("request", builder.Parent.ChangeStatusSchema, "The request to change content status.") .HasBody("request", builder.Parent.ChangeStatusSchema, "The request to change content status.")
.Responds(200, "Content status updated.", builder.ContentSchema) .Responds(200, "Content status updated.", builder.ContentSchema)
@ -175,7 +184,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
builder.AddOperation(OpenApiOperationMethod.Delete, "/{id}") builder.AddOperation(OpenApiOperationMethod.Delete, "/{id}")
.RequirePermission(Permissions.AppContentsDeleteOwn) .RequirePermission(Permissions.AppContentsDeleteOwn)
.Operation("Delete") .Operation("Delete")
.OperationSummary("Delete a schema content item.") .OperationSummary("Delete a [schema] content item.")
.HasId() .HasId()
.Responds(204, "Content item deleted"); .Responds(204, "Content item deleted");
} }

4
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs

@ -12,8 +12,6 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Squidex.Areas.Api.Controllers.Contents.Models namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
public sealed class BulkUpdateContentsDto public sealed class BulkUpdateContentsDto
@ -61,6 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray(); result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray();
#pragma warning disable CS0618 // Type or member is obsolete
if (result.Jobs != null && Publish) if (result.Jobs != null && Publish)
{ {
foreach (var job in result.Jobs) foreach (var job in result.Jobs)
@ -71,6 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
} }
} }
} }
#pragma warning restore CS0618 // Type or member is obsolete
return result; return result;
} }

10
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -138,13 +138,13 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
var app = resources.App; var app = resources.App;
var values = new { app, name = schema, id = Id }; var values = new { app, schema, id = Id };
AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContent), values)); AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContent), values));
if (Version > 0) if (Version > 0)
{ {
var versioned = new { app, name = schema, id = Id, version = Version - 1 }; var versioned = new { app, schema, id = Id, version = Version - 1 };
AddGetLink("previous", resources.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned)); AddGetLink("previous", resources.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned));
} }
@ -179,10 +179,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (content.CanUpdate && resources.CanUpdateContent(schema)) if (content.CanUpdate && resources.CanUpdateContent(schema))
{ {
AddPutLink("update", resources.Url<ContentsController>(x => nameof(x.PutContent), values));
AddPatchLink("patch", resources.Url<ContentsController>(x => nameof(x.PatchContent), values)); AddPatchLink("patch", resources.Url<ContentsController>(x => nameof(x.PatchContent), values));
} }
if (content.CanUpdate && resources.CanUpdateContent(schema))
{
AddPutLink("update", resources.Url<ContentsController>(x => nameof(x.PutContent), values));
}
return this; return this;
} }
} }

4
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
private void CreateLinks(Resources resources, string schema) private void CreateLinks(Resources resources, string schema)
{ {
var values = new { app = resources.App, name = schema }; var values = new { app = resources.App, schema };
AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContents), values)); AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContents), values));
@ -70,7 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
AddPostLink("create", resources.Url<ContentsController>(x => nameof(x.PostContent), values)); AddPostLink("create", resources.Url<ContentsController>(x => nameof(x.PostContent), values));
var publishValues = new { values.app, values.name, publish = true }; var publishValues = new { values.app, values.schema, publish = true };
AddPostLink("create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues)); AddPostLink("create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues));
} }

4
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs

@ -12,8 +12,6 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using StatusType = Squidex.Domain.Apps.Core.Contents.Status; using StatusType = Squidex.Domain.Apps.Core.Contents.Status;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Squidex.Areas.Api.Controllers.Contents.Models namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
public class CreateContentDto public class CreateContentDto
@ -56,10 +54,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
command.Status = Status; command.Status = Status;
} }
#pragma warning disable CS0618 // Type or member is obsolete
else if (Publish) else if (Publish)
{ {
command.Status = StatusType.Published; command.Status = StatusType.Published;
} }
#pragma warning restore CS0618 // Type or member is obsolete
return command; return command;
} }

4
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs

@ -13,8 +13,6 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Squidex.Areas.Api.Controllers.Contents.Models namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
public sealed class ImportContentsDto public sealed class ImportContentsDto
@ -47,6 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
result.Jobs = Datas?.Select(x => new BulkUpdateJob { Type = BulkUpdateContentType.Create, Data = x }).ToArray(); result.Jobs = Datas?.Select(x => new BulkUpdateJob { Type = BulkUpdateContentType.Create, Data = x }).ToArray();
#pragma warning disable CS0618 // Type or member is obsolete
if (result.Jobs != null && Publish) if (result.Jobs != null && Publish)
{ {
foreach (var job in result.Jobs) foreach (var job in result.Jobs)
@ -57,6 +56,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
} }
} }
} }
#pragma warning restore CS0618 // Type or member is obsolete
return result; return result;
} }

4
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs

@ -12,8 +12,6 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using StatusType = Squidex.Domain.Apps.Core.Contents.Status; using StatusType = Squidex.Domain.Apps.Core.Contents.Status;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Squidex.Areas.Api.Controllers.Contents.Models namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
public class UpsertContentDto public class UpsertContentDto
@ -45,10 +43,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
command.Status = Status; command.Status = Status;
} }
#pragma warning disable CS0618 // Type or member is obsolete
else if (Publish) else if (Publish)
{ {
command.Status = StatusType.Published; command.Status = StatusType.Published;
} }
#pragma warning restore CS0618 // Type or member is obsolete
return command; return command;
} }

10
backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -413,18 +413,18 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// <summary> /// <summary>
/// Provide the json schema for the event with the specified name. /// Provide the json schema for the event with the specified name.
/// </summary> /// </summary>
/// <param name="name">The name of the event.</param> /// <param name="type">The type name of the event.</param>
/// <returns> /// <returns>
/// 200 => Rule event type found. /// 200 => Rule event type found.
/// 404 => Rule event not found. /// 404 => Rule event not found.
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("rules/eventtypes/{name}")] [Route("rules/eventtypes/{type}")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)] [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[AllowAnonymous] [AllowAnonymous]
public IActionResult GetEventSchema(string name) public IActionResult GetEventSchema(string type)
{ {
var schema = eventJsonSchemaGenerator.GetSchema(name); var schema = eventJsonSchemaGenerator.GetSchema(type);
if (schema == null) if (schema == null)
{ {
@ -446,4 +446,4 @@ namespace Squidex.Areas.Api.Controllers.Rules
return response; return response;
} }
} }
} }

10
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs

@ -40,6 +40,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters
return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto()); return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto());
} }
public FieldPropertiesDto Visit(ComponentFieldProperties properties, None args)
{
return SimpleMapper.Map(properties, new ComponentFieldPropertiesDto());
}
public FieldPropertiesDto Visit(ComponentsFieldProperties properties, None args)
{
return SimpleMapper.Map(properties, new ComponentsFieldPropertiesDto());
}
public FieldPropertiesDto Visit(DateTimeFieldProperties properties, None args) public FieldPropertiesDto Visit(DateTimeFieldProperties properties, None args)
{ {
return SimpleMapper.Map(properties, new DateTimeFieldPropertiesDto()); return SimpleMapper.Map(properties, new DateTimeFieldPropertiesDto());

4
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs

@ -108,7 +108,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
if (allowUpdate) if (allowUpdate)
{ {
var values = new { app = resources.App, name = schema, id = FieldId }; var values = new { app = resources.App, schema, id = FieldId };
AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutField), values)); AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutField), values));
@ -132,7 +132,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
if (Properties is ArrayFieldPropertiesDto) if (Properties is ArrayFieldPropertiesDto)
{ {
var parentValues = new { values.app, values.name, parentId = FieldId }; var parentValues = new { values.app, values.schema, parentId = FieldId };
AddPostLink("fields/add", resources.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues)); AddPostLink("fields/add", resources.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues));

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

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
{
public sealed class ComponentFieldPropertiesDto : FieldPropertiesDto
{
/// <summary>
/// The id of the embedded schemas.
/// </summary>
public ImmutableList<DomainId>? SchemaIds { get; set; }
public override FieldProperties ToProperties()
{
var result = SimpleMapper.Map(this, new ComponentFieldProperties());
return result;
}
}
}

39
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
{
public sealed class ComponentsFieldPropertiesDto : FieldPropertiesDto
{
/// <summary>
/// The minimum allowed items for the field value.
/// </summary>
public int? MinItems { get; set; }
/// <summary>
/// The maximum allowed items for the field value.
/// </summary>
public int? MaxItems { get; set; }
/// <summary>
/// The id of the embedded schemas.
/// </summary>
public ImmutableList<DomainId>? SchemaIds { get; set; }
public override FieldProperties ToProperties()
{
var result = SimpleMapper.Map(this, new ComponentsFieldProperties());
return result;
}
}
}

2
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs

@ -51,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
if (allowUpdate) if (allowUpdate)
{ {
var values = new { app = resources.App, name = schema, parentId, id = FieldId }; var values = new { app = resources.App, schema, parentId, id = FieldId };
AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values)); AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values));

2
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs

@ -151,7 +151,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
protected virtual void CreateLinks(Resources resources) protected virtual void CreateLinks(Resources resources)
{ {
var values = new { app = resources.App, name = Name }; var values = new { app = resources.App, schema = Name };
var allowUpdate = resources.CanUpdateSchema(Name); var allowUpdate = resources.CanUpdateSchema(Name);

118
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -32,7 +32,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Add a schema field. /// Add a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The field object that needs to be added to the schema.</param> /// <param name="request">The field object that needs to be added to the schema.</param>
/// <returns> /// <returns>
/// 201 => Schema field created. /// 201 => Schema field created.
@ -41,24 +41,24 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 409 => Schema field name already in use. /// 409 => Schema field name already in use.
/// </returns> /// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/schemas/{name}/fields/")] [Route("apps/{app}/schemas/{schema}/fields/")]
[ProducesResponseType(typeof(SchemaDto), 201)] [ProducesResponseType(typeof(SchemaDto), 201)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostField(string app, string name, [FromBody] AddFieldDto request) public async Task<IActionResult> PostField(string app, string schema, [FromBody] AddFieldDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, name }, response); return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, schema }, response);
} }
/// <summary> /// <summary>
/// Add a nested field. /// Add a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="request">The field object that needs to be added to the schema.</param> /// <param name="request">The field object that needs to be added to the schema.</param>
/// <returns> /// <returns>
@ -68,24 +68,24 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema, field or app not found. /// 404 => Schema, field or app not found.
/// </returns> /// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/")]
[ProducesResponseType(typeof(SchemaDto), 201)] [ProducesResponseType(typeof(SchemaDto), 201)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostNestedField(string app, string name, long parentId, [FromBody] AddFieldDto request) public async Task<IActionResult> PostNestedField(string app, string schema, long parentId, [FromBody] AddFieldDto request)
{ {
var command = request.ToCommand(parentId); var command = request.ToCommand(parentId);
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, name }, response); return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, schema }, response);
} }
/// <summary> /// <summary>
/// Configure UI fields. /// Configure UI fields.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The request that contains the field names.</param> /// <param name="request">The request that contains the field names.</param>
/// <returns> /// <returns>
/// 200 => Schema UI fields defined. /// 200 => Schema UI fields defined.
@ -93,11 +93,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/ui/")] [Route("apps/{app}/schemas/{schema}/fields/ui/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutSchemaUIFields(string app, string name, [FromBody] ConfigureUIFieldsDto request) public async Task<IActionResult> PutSchemaUIFields(string app, string schema, [FromBody] ConfigureUIFieldsDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -110,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Reorder all fields. /// Reorder all fields.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The request that contains the field ids.</param> /// <param name="request">The request that contains the field ids.</param>
/// <returns> /// <returns>
/// 200 => Schema fields reordered. /// 200 => Schema fields reordered.
@ -118,11 +118,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/ordering/")] [Route("apps/{app}/schemas/{schema}/fields/ordering/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutSchemaFieldOrdering(string app, string name, [FromBody] ReorderFieldsDto request) public async Task<IActionResult> PutSchemaFieldOrdering(string app, string schema, [FromBody] ReorderFieldsDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -135,7 +135,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Reorder all nested fields. /// Reorder all nested fields.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="request">The request that contains the field ids.</param> /// <param name="request">The request that contains the field ids.</param>
/// <returns> /// <returns>
@ -144,11 +144,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema, field or app not found. /// 404 => Schema, field or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/ordering/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/ordering/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutNestedFieldOrdering(string app, string name, long parentId, [FromBody] ReorderFieldsDto request) public async Task<IActionResult> PutNestedFieldOrdering(string app, string schema, long parentId, [FromBody] ReorderFieldsDto request)
{ {
var command = request.ToCommand(parentId); var command = request.ToCommand(parentId);
@ -161,7 +161,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Update a schema field. /// Update a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the field to update.</param> /// <param name="id">The id of the field to update.</param>
/// <param name="request">The field object that needs to be added to the schema.</param> /// <param name="request">The field object that needs to be added to the schema.</param>
/// <returns> /// <returns>
@ -170,11 +170,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema, field or app not found. /// 404 => Schema, field or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutField(string app, string name, long id, [FromBody] UpdateFieldDto request) public async Task<IActionResult> PutField(string app, string schema, long id, [FromBody] UpdateFieldDto request)
{ {
var command = request.ToCommand(id); var command = request.ToCommand(id);
@ -187,7 +187,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Update a nested field. /// Update a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="id">The id of the field to update.</param> /// <param name="id">The id of the field to update.</param>
/// <param name="request">The field object that needs to be added to the schema.</param> /// <param name="request">The field object that needs to be added to the schema.</param>
@ -197,11 +197,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema, field or app not found. /// 404 => Schema, field or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutNestedField(string app, string name, long parentId, long id, [FromBody] UpdateFieldDto request) public async Task<IActionResult> PutNestedField(string app, string schema, long parentId, long id, [FromBody] UpdateFieldDto request)
{ {
var command = request.ToCommand(id, parentId); var command = request.ToCommand(id, parentId);
@ -214,7 +214,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Lock a schema field. /// Lock a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the field to lock.</param> /// <param name="id">The id of the field to lock.</param>
/// <returns> /// <returns>
/// 200 => Schema field shown. /// 200 => Schema field shown.
@ -225,11 +225,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A locked field cannot be updated or deleted. /// A locked field cannot be updated or deleted.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/lock/")] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/lock/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> LockField(string app, string name, long id) public async Task<IActionResult> LockField(string app, string schema, long id)
{ {
var command = new LockField { FieldId = id }; var command = new LockField { FieldId = id };
@ -242,7 +242,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Lock a nested field. /// Lock a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="id">The id of the field to lock.</param> /// <param name="id">The id of the field to lock.</param>
/// <returns> /// <returns>
@ -254,11 +254,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A locked field cannot be edited or deleted. /// A locked field cannot be edited or deleted.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/lock/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/lock/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> LockNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> LockNestedField(string app, string schema, long parentId, long id)
{ {
var command = new LockField { ParentFieldId = parentId, FieldId = id }; var command = new LockField { ParentFieldId = parentId, FieldId = id };
@ -271,7 +271,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Hide a schema field. /// Hide a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the field to hide.</param> /// <param name="id">The id of the field to hide.</param>
/// <returns> /// <returns>
/// 200 => Schema field hidden. /// 200 => Schema field hidden.
@ -282,11 +282,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A hidden field is not part of the API response, but can still be edited in the portal. /// A hidden field is not part of the API response, but can still be edited in the portal.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/hide/")] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/hide/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> HideField(string app, string name, long id) public async Task<IActionResult> HideField(string app, string schema, long id)
{ {
var command = new HideField { FieldId = id }; var command = new HideField { FieldId = id };
@ -299,7 +299,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Hide a nested field. /// Hide a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="id">The id of the field to hide.</param> /// <param name="id">The id of the field to hide.</param>
/// <returns> /// <returns>
@ -311,11 +311,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A hidden field is not part of the API response, but can still be edited in the portal. /// A hidden field is not part of the API response, but can still be edited in the portal.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/hide/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/hide/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> HideNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> HideNestedField(string app, string schema, long parentId, long id)
{ {
var command = new HideField { ParentFieldId = parentId, FieldId = id }; var command = new HideField { ParentFieldId = parentId, FieldId = id };
@ -328,7 +328,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Show a schema field. /// Show a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the field to show.</param> /// <param name="id">The id of the field to show.</param>
/// <returns> /// <returns>
/// 200 => Schema field shown. /// 200 => Schema field shown.
@ -339,11 +339,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A hidden field is not part of the API response, but can still be edited in the portal. /// A hidden field is not part of the API response, but can still be edited in the portal.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/show/")] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/show/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> ShowField(string app, string name, long id) public async Task<IActionResult> ShowField(string app, string schema, long id)
{ {
var command = new ShowField { FieldId = id }; var command = new ShowField { FieldId = id };
@ -356,7 +356,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Show a nested field. /// Show a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="id">The id of the field to show.</param> /// <param name="id">The id of the field to show.</param>
/// <returns> /// <returns>
@ -368,11 +368,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A hidden field is not part of the API response, but can still be edited in the portal. /// A hidden field is not part of the API response, but can still be edited in the portal.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/show/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/show/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> ShowNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> ShowNestedField(string app, string schema, long parentId, long id)
{ {
var command = new ShowField { ParentFieldId = parentId, FieldId = id }; var command = new ShowField { ParentFieldId = parentId, FieldId = id };
@ -385,7 +385,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Enable a schema field. /// Enable a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the field to enable.</param> /// <param name="id">The id of the field to enable.</param>
/// <returns> /// <returns>
/// 200 => Schema field enabled. /// 200 => Schema field enabled.
@ -396,11 +396,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/enable/")] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/enable/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> EnableField(string app, string name, long id) public async Task<IActionResult> EnableField(string app, string schema, long id)
{ {
var command = new EnableField { FieldId = id }; var command = new EnableField { FieldId = id };
@ -413,7 +413,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Enable a nested field. /// Enable a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="id">The id of the field to enable.</param> /// <param name="id">The id of the field to enable.</param>
/// <returns> /// <returns>
@ -425,11 +425,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/enable/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/enable/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> EnableNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> EnableNestedField(string app, string schema, long parentId, long id)
{ {
var command = new EnableField { ParentFieldId = parentId, FieldId = id }; var command = new EnableField { ParentFieldId = parentId, FieldId = id };
@ -442,7 +442,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Disable a schema field. /// Disable a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the field to disable.</param> /// <param name="id">The id of the field to disable.</param>
/// <returns> /// <returns>
/// 200 => Schema field disabled. /// 200 => Schema field disabled.
@ -453,11 +453,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/disable/")] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/disable/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DisableField(string app, string name, long id) public async Task<IActionResult> DisableField(string app, string schema, long id)
{ {
var command = new DisableField { FieldId = id }; var command = new DisableField { FieldId = id };
@ -470,7 +470,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Disable a nested field. /// Disable a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="id">The id of the field to disable.</param> /// <param name="id">The id of the field to disable.</param>
/// <returns> /// <returns>
@ -482,11 +482,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response.
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/disable/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/disable/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DisableNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> DisableNestedField(string app, string schema, long parentId, long id)
{ {
var command = new DisableField { ParentFieldId = parentId, FieldId = id }; var command = new DisableField { ParentFieldId = parentId, FieldId = id };
@ -499,7 +499,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Delete a schema field. /// Delete a schema field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The id of the field to disable.</param> /// <param name="id">The id of the field to disable.</param>
/// <returns> /// <returns>
/// 200 => Schema field deleted. /// 200 => Schema field deleted.
@ -507,11 +507,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema, field or app not found. /// 404 => Schema, field or app not found.
/// </returns> /// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteField(string app, string name, long id) public async Task<IActionResult> DeleteField(string app, string schema, long id)
{ {
var command = new DeleteField { FieldId = id }; var command = new DeleteField { FieldId = id };
@ -524,7 +524,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Delete a nested field. /// Delete a nested field.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="parentId">The parent field id.</param> /// <param name="parentId">The parent field id.</param>
/// <param name="id">The id of the field to disable.</param> /// <param name="id">The id of the field to disable.</param>
/// <returns> /// <returns>
@ -533,11 +533,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema, field or app not found. /// 404 => Schema, field or app not found.
/// </returns> /// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> DeleteNestedField(string app, string schema, long parentId, long id)
{ {
var command = new DeleteField { ParentFieldId = parentId, FieldId = id }; var command = new DeleteField { ParentFieldId = parentId, FieldId = id };

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

@ -70,31 +70,24 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Get a schema by name. /// Get a schema by name.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema to retrieve.</param> /// <param name="schema">The name of the schema to retrieve.</param>
/// <returns> /// <returns>
/// 200 => Schema found. /// 200 => Schema found.
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/schemas/{name}/")] [Route("apps/{app}/schemas/{schema}/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasRead)] [ApiPermissionOrAnonymous(Permissions.AppSchemasRead)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetSchema(string app, string name) public IActionResult GetSchema(string app, string schema)
{ {
var schema = await GetSchemaAsync(name);
if (schema == null)
{
return NotFound();
}
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return SchemaDto.FromSchema(schema, Resources); return SchemaDto.FromSchema(Schema, Resources);
}); });
Response.Headers[HeaderNames.ETag] = schema.ToEtag(); Response.Headers[HeaderNames.ETag] = Schema.ToEtag();
return Ok(response); return Ok(response);
} }
@ -120,14 +113,14 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetSchema), new { app, name = request.Name }, response); return CreatedAtAction(nameof(GetSchema), new { app, schema = request.Name }, response);
} }
/// <summary> /// <summary>
/// Update a schema. /// Update a schema.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param> /// <param name="request">The schema object that needs to updated.</param>
/// <returns> /// <returns>
/// 200 => Schema updated. /// 200 => Schema updated.
@ -135,11 +128,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/")] [Route("apps/{app}/schemas/{schema}/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) public async Task<IActionResult> PutSchema(string app, string schema, [FromBody] UpdateSchemaDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -152,7 +145,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Synchronize a schema. /// Synchronize a schema.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param> /// <param name="request">The schema object that needs to updated.</param>
/// <returns> /// <returns>
/// 200 => Schema updated. /// 200 => Schema updated.
@ -160,11 +153,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/sync")] [Route("apps/{app}/schemas/{schema}/sync")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request) public async Task<IActionResult> PutSchemaSync(string app, string schema, [FromBody] SynchronizeSchemaDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -177,7 +170,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Update a schema category. /// Update a schema category.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param> /// <param name="request">The schema object that needs to updated.</param>
/// <returns> /// <returns>
/// 200 => Schema updated. /// 200 => Schema updated.
@ -185,11 +178,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/category")] [Route("apps/{app}/schemas/{schema}/category")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutCategory(string app, string name, [FromBody] ChangeCategoryDto request) public async Task<IActionResult> PutCategory(string app, string schema, [FromBody] ChangeCategoryDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -202,7 +195,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Update the preview urls. /// Update the preview urls.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The preview urls for the schema.</param> /// <param name="request">The preview urls for the schema.</param>
/// <returns> /// <returns>
/// 200 => Schema updated. /// 200 => Schema updated.
@ -210,11 +203,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/preview-urls")] [Route("apps/{app}/schemas/{schema}/preview-urls")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request) public async Task<IActionResult> PutPreviewUrls(string app, string schema, [FromBody] ConfigurePreviewUrlsDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -227,7 +220,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Update the scripts. /// Update the scripts.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The schema scripts object that needs to updated.</param> /// <param name="request">The schema scripts object that needs to updated.</param>
/// <returns> /// <returns>
/// 200 => Schema updated. /// 200 => Schema updated.
@ -235,11 +228,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/scripts/")] [Route("apps/{app}/schemas/{schema}/scripts/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasScripts)] [ApiPermissionOrAnonymous(Permissions.AppSchemasScripts)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutScripts(string app, string name, [FromBody] SchemaScriptsDto request) public async Task<IActionResult> PutScripts(string app, string schema, [FromBody] SchemaScriptsDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -252,7 +245,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Update the rules. /// Update the rules.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The schema rules object that needs to updated.</param> /// <param name="request">The schema rules object that needs to updated.</param>
/// <returns> /// <returns>
/// 200 => Schema updated. /// 200 => Schema updated.
@ -260,11 +253,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/rules/")] [Route("apps/{app}/schemas/{schema}/rules/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutRules(string app, string name, [FromBody] ConfigureFieldRulesDto request) public async Task<IActionResult> PutRules(string app, string schema, [FromBody] ConfigureFieldRulesDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
@ -277,17 +270,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Publish a schema. /// Publish a schema.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema to publish.</param> /// <param name="schema">The name of the schema to publish.</param>
/// <returns> /// <returns>
/// 200 => Schema published. /// 200 => Schema published.
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/publish/")] [Route("apps/{app}/schemas/{schema}/publish/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)] [ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PublishSchema(string app, string name) public async Task<IActionResult> PublishSchema(string app, string schema)
{ {
var command = new PublishSchema(); var command = new PublishSchema();
@ -300,17 +293,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Unpublish a schema. /// Unpublish a schema.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema to unpublish.</param> /// <param name="schema">The name of the schema to unpublish.</param>
/// <returns> /// <returns>
/// 200 => Schema unpublished. /// 200 => Schema unpublished.
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/schemas/{name}/unpublish/")] [Route("apps/{app}/schemas/{schema}/unpublish/")]
[ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)] [ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> UnpublishSchema(string app, string name) public async Task<IActionResult> UnpublishSchema(string app, string schema)
{ {
var command = new UnpublishSchema(); var command = new UnpublishSchema();
@ -323,16 +316,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// Delete a schema. /// Delete a schema.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema to delete.</param> /// <param name="schema">The name of the schema to delete.</param>
/// <returns> /// <returns>
/// 204 => Schema deleted. /// 204 => Schema deleted.
/// 404 => Schema or app not found. /// 404 => Schema or app not found.
/// </returns> /// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/schemas/{name}/")] [Route("apps/{app}/schemas/{schema}/")]
[ApiPermissionOrAnonymous(Permissions.AppSchemasDelete)] [ApiPermissionOrAnonymous(Permissions.AppSchemasDelete)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteSchema(string app, string name) public async Task<IActionResult> DeleteSchema(string app, string schema)
{ {
await CommandBus.PublishAsync(new DeleteSchema()); await CommandBus.PublishAsync(new DeleteSchema());
@ -340,30 +333,23 @@ namespace Squidex.Areas.Api.Controllers.Schemas
} }
[HttpGet] [HttpGet]
[Route("apps/{app}/schemas/{name}/completion")] [Route("apps/{app}/schemas/{schema}/completion")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
[OpenApiIgnore] [OpenApiIgnore]
public async Task<IActionResult> GetScriptCompletiong(string app, string name) public IActionResult GetScriptCompletion(string app, string schema)
{ {
var schema = await GetSchemaAsync(name);
if (schema == null)
{
return NotFound();
}
var completer = new ScriptingCompletion(); var completer = new ScriptingCompletion();
var completion = completer.GetCompletion(schema.SchemaDef, App.PartitionResolver()); var completion = completer.GetCompletion(Schema.SchemaDef, App.PartitionResolver());
var result = completion.Select(x => new { x.Name, x.Description }); var result = completion.Select(x => new { x.Name, x.Description });
return Ok(result); return Ok(result);
} }
private Task<ISchemaEntity?> GetSchemaAsync(string name) private Task<ISchemaEntity?> GetSchemaAsync(string schema)
{ {
if (Guid.TryParse(name, out var guid)) if (Guid.TryParse(schema, out var guid))
{ {
var schemaId = DomainId.Create(guid); var schemaId = DomainId.Create(guid);
@ -371,7 +357,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
} }
else else
{ {
return appProvider.GetSchemaAsync(AppId, name); return appProvider.GetSchemaAsync(AppId, schema);
} }
} }

12
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs

@ -458,19 +458,19 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
public void Should_serialize_and_deserialize_schema() public void Should_serialize_and_deserialize_schema()
{ {
var schemaSource = var schemaSource =
TestUtils.MixedSchema(SchemaType.Singleton) TestUtils.MixedSchema(SchemaType.Singleton, false)
.ChangeCategory("Category") .ChangeCategory("Category")
.SetFieldRules(FieldRule.Hide("2")) .SetFieldRules(FieldRule.Hide("2"))
.SetFieldsInLists("field2") .SetFieldsInLists("field2")
.SetFieldsInReferences("field1") .SetFieldsInReferences("field1")
.SetPreviewUrls(new Dictionary<string, string>
{
["web"] = "Url"
}.ToImmutableDictionary())
.SetScripts(new SchemaScripts .SetScripts(new SchemaScripts
{ {
Create = "<create-script>" Create = "<create-script>"
}); })
.SetPreviewUrls(new Dictionary<string, string>
{
["web"] = "Url"
}.ToImmutableDictionary());
var schemaTarget = schemaSource.SerializeAndDeserialize(); var schemaTarget = schemaSource.SerializeAndDeserialize();

124
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs

@ -8,6 +8,8 @@
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ConvertContent namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
@ -20,60 +22,102 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{ {
schema = schema =
new Schema("my-schema") new Schema("my-schema")
.AddNumber(1, "field1", Partitioning.Language) .AddComponent(1, "component", Partitioning.Invariant)
.AddNumber(2, "field2", Partitioning.Invariant) .AddComponents(2, "components", Partitioning.Invariant)
.AddNumber(3, "field3", Partitioning.Invariant) .AddAssets(3, "assets1", Partitioning.Invariant)
.AddAssets(5, "assets1", Partitioning.Invariant) .AddAssets(4, "assets2", Partitioning.Invariant)
.AddAssets(6, "assets2", Partitioning.Invariant) .AddReferences(5, "references", Partitioning.Invariant)
.AddArray(7, "array", Partitioning.Invariant, h => h .AddArray(6, "array", Partitioning.Invariant, a => a
.AddNumber(71, "nested1") .AddAssets(31, "nested"));
.AddNumber(72, "nested2"))
.AddJson(4, "json", Partitioning.Language) schema.FieldsById[1].SetResolvedSchema(DomainId.Empty, schema);
.HideField(2) schema.FieldsById[2].SetResolvedSchema(DomainId.Empty, schema);
.HideField(71, 7)
.UpdateField(3, f => f.Hide());
} }
[Fact] [Fact]
public void Should_convert_name_to_name() public void Should_apply_value_conversion_on_all_levels()
{ {
var input = var source =
new ContentData() new ContentData()
.AddField("field1", .AddField("references",
new ContentFieldData() new ContentFieldData()
.AddLocalized("en", "EN")) .AddInvariant(JsonValue.Array(1, 2)))
.AddField("field2", .AddField("assets1",
new ContentFieldData() new ContentFieldData()
.AddInvariant(1)) .AddInvariant(JsonValue.Array(1)))
.AddField("invalid", .AddField("array",
new ContentFieldData() new ContentFieldData()
.AddInvariant(2)); .AddInvariant(
JsonValue.Array(
var actual = input.Convert(schema, (data, field) => field.Name == "field2" ? null : data); JsonValue.Object()
.Add("nested", JsonValue.Array(1, 2)))))
.AddField("component",
new ContentFieldData()
.AddInvariant(
JsonValue.Object()
.Add("references",
JsonValue.Array(1, 2))
.Add("assets1",
JsonValue.Array(1))
.Add("array",
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(1, 2))))
.Add(Component.Discriminator, DomainId.Empty)))
.AddField("components",
new ContentFieldData()
.AddInvariant(
JsonValue.Array(
JsonValue.Object()
.Add("references",
JsonValue.Array(1, 2))
.Add("assets1",
JsonValue.Array(1))
.Add("array",
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(1, 2))))
.Add(Component.Discriminator, DomainId.Empty))));
var expected = var expected =
new ContentData() new ContentData()
.AddField("field1", .AddField("references",
new ContentFieldData())
.AddField("assets1",
new ContentFieldData() new ContentFieldData()
.AddLocalized("en", "EN")); .AddInvariant(JsonValue.Array(1)))
.AddField("array",
Assert.Equal(expected, actual); new ContentFieldData()
} .AddInvariant(
JsonValue.Array(
[Fact] JsonValue.Object())))
public void Should_be_equal_fields_if_they_have_same_value() .AddField("component",
{ new ContentFieldData()
var lhs = .AddInvariant(
new ContentFieldData() JsonValue.Object()
.AddInvariant(2); .Add("assets1",
JsonValue.Array(1))
.Add("array",
JsonValue.Array(
JsonValue.Object()))
.Add(Component.Discriminator, DomainId.Empty)))
.AddField("components",
new ContentFieldData()
.AddInvariant(
JsonValue.Array(
JsonValue.Object()
.Add("assets1",
JsonValue.Array(1))
.Add("array",
JsonValue.Array(
JsonValue.Object()))
.Add(Component.Discriminator, DomainId.Empty))));
var rhs = var actual =
new ContentFieldData() source.Convert(schema,
.AddInvariant(2); FieldConverters.ForValues((data, field, parent) => field.Name != "assets1" ? null : data));
Assert.True(lhs.Equals(rhs)); Assert.Equal(expected, actual);
Assert.True(lhs.Equals((object)rhs));
Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode());
} }
} }
} }

48
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs

@ -166,6 +166,54 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
Assert.Equal("No", formatted); Assert.Equal("No", formatted);
} }
[Fact]
public void Should_format_component_field()
{
var value = JsonValue.Object();
var field = Fields.Component(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
Assert.Equal("{ Component }", formatted);
}
[Fact]
public void Should_format_components_field_without_items()
{
var value = JsonValue.Array();
var field = Fields.Components(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
Assert.Equal("0 Components", formatted);
}
[Fact]
public void Should_format_components_field_with_single_item()
{
var value = JsonValue.Array(JsonValue.Object());
var field = Fields.Components(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
Assert.Equal("1 Component", formatted);
}
[Fact]
public void Should_format_components_field_with_multiple_items()
{
var value = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var field = Fields.Components(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
Assert.Equal("2 Components", formatted);
}
[Fact] [Fact]
public void Should_format_datetime_field() public void Should_format_datetime_field()
{ {

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

@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{ {
var source = JsonValue.Create(123); var source = JsonValue.Create(123);
var result = ValueConverters.ExcludeHidden(source, stringField.Hide()); var result = ValueConverters.ExcludeHidden(source, stringField.Hide(), null);
Assert.Null(result); Assert.Null(result);
} }
@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{ {
var source = JsonValue.Create("invalid"); var source = JsonValue.Create("invalid");
var result = ValueConverters.ExcludeChangedTypes(TestUtils.DefaultSerializer)(source, numberField); var result = ValueConverters.ExcludeChangedTypes(TestUtils.DefaultSerializer)(source, numberField, null);
Assert.Null(result); Assert.Null(result);
} }
@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var expected = JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"); var expected = JsonValue.Array($"url/to/{id1}", $"url/to/{id2}");
var result = ValueConverters.ResolveAssetUrls(appId, HashSet.Of(path), urlGenerator)(source, field); var result = ValueConverters.ResolveAssetUrls(appId, HashSet.Of(path), urlGenerator)(source, field, null);
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var expected = source; var expected = source;
var result = ValueConverters.ResolveAssetUrls(appId, HashSet.Of(path), urlGenerator)(source, field); var result = ValueConverters.ResolveAssetUrls(appId, HashSet.Of(path), urlGenerator)(source, field, null);
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }

87
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs

@ -25,10 +25,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
{ {
schema = schema =
new Schema("my-schema") new Schema("my-schema")
.AddReferences(1, "references", Partitioning.Invariant) .AddComponent(1, "component", Partitioning.Invariant)
.AddAssets(2, "assets", Partitioning.Invariant) .AddComponents(2, "components", Partitioning.Invariant)
.AddArray(3, "array", Partitioning.Invariant, a => a .AddAssets(3, "assets1", Partitioning.Invariant)
.AddAssets(4, "assets2", Partitioning.Invariant)
.AddReferences(5, "references", Partitioning.Invariant)
.AddArray(6, "array", Partitioning.Invariant, a => a
.AddAssets(31, "nested")); .AddAssets(31, "nested"));
schema.FieldsById[1].SetResolvedSchema(DomainId.Empty, schema);
schema.FieldsById[2].SetResolvedSchema(DomainId.Empty, schema);
} }
[Fact] [Fact]
@ -39,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var input = var input =
new ContentData() new ContentData()
.AddField("assets", .AddField("assets1",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString()))); .AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString())));
@ -58,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var input = var input =
new ContentData() new ContentData()
.AddField("assets", .AddField("assets1",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString()))); .AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString())));
@ -80,7 +86,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
.AddField("references", .AddField("references",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.Array(id1, id2))) .AddInvariant(JsonValue.Array(id1, id2)))
.AddField("assets", .AddField("assets1",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.Array(id1))) .AddInvariant(JsonValue.Array(id1)))
.AddField("array", .AddField("array",
@ -88,14 +94,41 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
.AddInvariant( .AddInvariant(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("nested", JsonValue.Array(id1, id2))))); .Add("nested", JsonValue.Array(id1, id2)))))
.AddField("component",
new ContentFieldData()
.AddInvariant(
JsonValue.Object()
.Add("references",
JsonValue.Array(id1, id2))
.Add("assets1",
JsonValue.Array(id1))
.Add("array",
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id1, id2))))
.Add(Component.Discriminator, DomainId.Empty)))
.AddField("components",
new ContentFieldData()
.AddInvariant(
JsonValue.Array(
JsonValue.Object()
.Add("references",
JsonValue.Array(id1, id2))
.Add("assets1",
JsonValue.Array(id1))
.Add("array",
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id1, id2))))
.Add(Component.Discriminator, DomainId.Empty))));
var expected = var expected =
new ContentData() new ContentData()
.AddField("references", .AddField("references",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.Array(id2))) .AddInvariant(JsonValue.Array(id2)))
.AddField("assets", .AddField("assets1",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.Array())) .AddInvariant(JsonValue.Array()))
.AddField("array", .AddField("array",
@ -103,12 +136,38 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
.AddInvariant( .AddInvariant(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("nested", JsonValue.Array(id2))))); .Add("nested", JsonValue.Array(id2)))))
.AddField("component",
var cleaner = ValueReferencesConverter.CleanReferences(new HashSet<DomainId> { id2 }); new ContentFieldData()
var cleanNested = ValueConverters.ForNested(cleaner); .AddInvariant(
JsonValue.Object()
var converter = FieldConverters.ForValues(cleaner, cleanNested); .Add("references",
JsonValue.Array(id2))
.Add("assets1",
JsonValue.Array())
.Add("array",
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id2))))
.Add(Component.Discriminator, DomainId.Empty)))
.AddField("components",
new ContentFieldData()
.AddInvariant(
JsonValue.Array(
JsonValue.Object()
.Add("references",
JsonValue.Array(id2))
.Add("assets1",
JsonValue.Array())
.Add("array",
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id2))))
.Add(Component.Discriminator, DomainId.Empty))));
var converter =
FieldConverters.ForValues(
ValueReferencesConverter.CleanReferences(new HashSet<DomainId> { id2 }));
var actual = source.Convert(schema, converter); var actual = source.Convert(schema, converter);

1
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

@ -18,6 +18,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
{ {
public class JsonSchemaTests public class JsonSchemaTests
{ {
private const int MaxDepth = 5;
private readonly Schema schema = TestUtils.MixedSchema(); private readonly Schema schema = TestUtils.MixedSchema();
[Fact] [Fact]

137
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs

@ -0,0 +1,137 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
public class ComponentFieldTests : IClassFixture<TranslationsFixture>
{
private readonly List<string> errors = new List<string>();
[Fact]
public void Should_instantiate_field()
{
var (_, sut) = Field(new ComponentFieldProperties());
Assert.Equal("my-component", sut.Name);
}
[Fact]
public async Task Should_not_add_error_if_component_is_null_and_valid()
{
var (_, sut) = Field(new ComponentFieldProperties());
await sut.ValidateAsync(null, errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_component_is_valid()
{
var (id, sut) = Field(new ComponentFieldProperties());
await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", 1), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_component_is_required()
{
var (_, sut) = Field(new ComponentFieldProperties { IsRequired = true });
await sut.ValidateAsync(null, errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
}
[Fact]
public async Task Should_add_error_if_component_value_is_required()
{
var (id, sut) = Field(new ComponentFieldProperties { IsRequired = true }, true);
await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", null), errors);
errors.Should().BeEquivalentTo(
new[] { "component-field: Field is required." });
}
[Fact]
public async Task Should_add_error_if_value_is_not_valid()
{
var (_, sut) = Field(new ComponentFieldProperties());
await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid json object, expected object with 'schemaId' field." });
}
[Fact]
public async Task Should_add_error_if_value_has_no_discriminator()
{
var (_, sut) = Field(new ComponentFieldProperties());
await sut.ValidateAsync(CreateValue(null, "field", 1), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid component. No 'schemaId' field found." });
}
[Fact]
public async Task Should_add_error_if_value_has_invalid_discriminator()
{
var (_, sut) = Field(new ComponentFieldProperties());
await sut.ValidateAsync(CreateValue("invalid", "field", 1), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid component. Cannot find schema." });
}
private static IJsonValue CreateValue(string? type, string key, object? value)
{
var obj = JsonValue.Object();
if (type != null)
{
obj[Component.Discriminator] = JsonValue.Create(type);
}
obj.Add(key, value);
return obj;
}
private static (DomainId, RootField<ComponentFieldProperties>) Field(ComponentFieldProperties properties, bool isRequired = false)
{
var schema =
new Schema("my-component")
.AddNumber(1, "component-field", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = isRequired });
var id = DomainId.NewGuid();
var field =
Fields.Component(1, "my-component", Partitioning.Invariant, properties)
.SetResolvedSchema(id, schema);
return (id, field);
}
}
}

187
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs

@ -0,0 +1,187 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
public class ComponentsFieldTests : IClassFixture<TranslationsFixture>
{
private readonly List<string> errors = new List<string>();
[Fact]
public void Should_instantiate_field()
{
var (_, sut) = Field(new ComponentsFieldProperties());
Assert.Equal("my-components", sut.Name);
}
[Fact]
public async Task Should_not_add_error_if_components_are_null_and_valid()
{
var (_, sut) = Field(new ComponentsFieldProperties());
await sut.ValidateAsync(null, errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_components_is_valid()
{
var (id, sut) = Field(new ComponentsFieldProperties());
await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", 1), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_number_of_components_is_equal_to_min_and_max_components()
{
var (id, sut) = Field(new ComponentsFieldProperties { MinItems = 2, MaxItems = 2 });
await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_components_are_required()
{
var (_, sut) = Field(new ComponentsFieldProperties { IsRequired = true });
await sut.ValidateAsync(null, errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
}
[Fact]
public async Task Should_add_error_if_components_value_is_required()
{
var (id, sut) = Field(new ComponentsFieldProperties { IsRequired = true }, true);
await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", null), errors);
errors.Should().BeEquivalentTo(
new[] { "[1].component-field: Field is required." });
}
[Fact]
public async Task Should_add_error_if_value_is_not_valid()
{
var (_, sut) = Field(new ComponentsFieldProperties());
await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid json type, expected array of objects." });
}
[Fact]
public async Task Should_add_error_if_component_is_not_valid()
{
var (_, sut) = Field(new ComponentsFieldProperties());
await sut.ValidateAsync(JsonValue.Array(JsonValue.Create("Invalid")), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid json object, expected object with 'schemaId' field." });
}
[Fact]
public async Task Should_add_error_if_component_has_no_discriminator()
{
var (_, sut) = Field(new ComponentsFieldProperties());
await sut.ValidateAsync(CreateValue(1, null, "field", 1), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid component. No 'schemaId' field found." });
}
[Fact]
public async Task Should_add_error_if_value_has_invalid_discriminator()
{
var (_, sut) = Field(new ComponentsFieldProperties());
await sut.ValidateAsync(CreateValue(1, "invalid", "field", 1), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid component. Cannot find schema." });
}
[Fact]
public async Task Should_add_error_if_value_has_not_enough_components()
{
var (id, sut) = Field(new ComponentsFieldProperties { MinItems = 3 });
await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." });
}
[Fact]
public async Task Should_add_error_if_value_has_too_much_components()
{
var (id, sut) = Field(new ComponentsFieldProperties { MaxItems = 1 });
await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." });
}
private static IJsonValue CreateValue(int count, string? type, string key, object? value)
{
var result = JsonValue.Array();
for (var i = 0; i < count; i++)
{
var obj = JsonValue.Object();
if (type != null)
{
obj[Component.Discriminator] = JsonValue.Create(type);
}
obj.Add(key, value);
result.Add(obj);
}
return result;
}
private static (DomainId, RootField<ComponentsFieldProperties>) Field(ComponentsFieldProperties properties, bool isRequired = false)
{
var schema =
new Schema("my-component")
.AddNumber(1, "component-field", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = isRequired });
var id = DomainId.NewGuid();
var field =
Fields.Components(1, "my-components", Partitioning.Invariant, properties)
.SetResolvedSchema(id, schema);
return (id, field);
}
}
}

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

@ -26,8 +26,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
private static readonly NamedId<DomainId> AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); private static readonly NamedId<DomainId> AppId = NamedId.Of(DomainId.NewGuid(), "my-app");
private static readonly NamedId<DomainId> SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); private static readonly NamedId<DomainId> SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
private static readonly ISemanticLog Log = A.Fake<ISemanticLog>();
private static readonly IValidatorsFactory Factory = new DefaultValidatorsFactory();
public static Task ValidateAsync(this IValidator validator, object? value, IList<string> errors, public static Task ValidateAsync(this IValidator validator, object? value, IList<string> errors,
Schema? schema = null, Schema? schema = null,
@ -49,11 +47,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var context = CreateContext(schema, mode, updater, action); var context = CreateContext(schema, mode, updater, action);
var validators = Factories(factory).SelectMany(x => x.CreateValueValidators(context, field, null!)).ToArray(); var validator = new ValidatorBuilder(factory, context).ValueValidator(field);
var validator = new AggregateValidator(validators, Log);
return new FieldValidator(validator, field) return validator.ValidateAsync(value, context, CreateFormatter(errors));
.ValidateAsync(value, context, CreateFormatter(errors));
} }
public static async Task ValidatePartialAsync(this ContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors, public static async Task ValidatePartialAsync(this ContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors,
@ -65,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var context = CreateContext(schema, mode, updater, action); var context = CreateContext(schema, mode, updater, action);
var validator = new ContentValidator(partitionResolver, context, Factories(factory), Log); var validator = new ValidatorBuilder(factory, context).ContentValidator(partitionResolver);
await validator.ValidateInputPartialAsync(data); await validator.ValidateInputPartialAsync(data);
@ -84,7 +80,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var context = CreateContext(schema, mode, updater, action); var context = CreateContext(schema, mode, updater, action);
var validator = new ContentValidator(partitionResolver, context, Factories(factory), Log); var validator = new ValidatorBuilder(factory, context).ContentValidator(partitionResolver);
await validator.ValidateInputAsync(data); await validator.ValidateInputAsync(data);
@ -109,18 +105,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
}; };
} }
private static IEnumerable<IValidatorsFactory> Factories(IValidatorsFactory? factory)
{
var result = Enumerable.Repeat(Factory, 1);
if (factory != null)
{
result = result.Union(Enumerable.Repeat(factory, 1));
}
return result;
}
public static ValidationContext CreateContext( public static ValidationContext CreateContext(
Schema? schema = null, Schema? schema = null,
ValidationMode mode = ValidationMode.Default, ValidationMode mode = ValidationMode.Default,
@ -143,5 +127,49 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
return context; return context;
} }
private sealed class ValidatorBuilder
{
private static readonly ISemanticLog Log = A.Fake<ISemanticLog>();
private static readonly IValidatorsFactory Default = new DefaultValidatorsFactory();
private readonly IValidatorsFactory? validatorFactory;
private readonly ValidationContext validationContext;
public ValidatorBuilder(IValidatorsFactory? validatorFactory, ValidationContext validationContext)
{
this.validatorFactory = validatorFactory;
this.validationContext = validationContext;
}
public ContentValidator ContentValidator(PartitionResolver partitionResolver)
{
return new ContentValidator(partitionResolver, validationContext, CreateFactories(), Log);
}
public IValidator ValueValidator(IField field)
{
return CreateValueValidator(field);
}
private IValidator CreateValueValidator(IField field)
{
return new FieldValidator(new AggregateValidator(CreateValueValidators(field), Log), field);
}
private IEnumerable<IValidator> CreateValueValidators(IField field)
{
return CreateFactories().SelectMany(x => x.CreateValueValidators(validationContext, field, CreateValueValidator));
}
private IEnumerable<IValidatorsFactory> CreateFactories()
{
yield return Default;
if (validatorFactory != null)
{
yield return validatorFactory;
}
}
}
} }
} }

65
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs

@ -0,0 +1,65 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
public class ComponentValidatorTests : IClassFixture<TranslationsFixture>
{
private readonly List<string> errors = new List<string>();
[Fact]
public async Task Should_create_validator_from_component_and_invoke()
{
var validator = A.Fake<IValidator>();
var componentData = JsonValue.Object();
var componentObject = new Component("type", componentData, new Schema("my-schema"));
var isFactoryCalled = false;
var sut = new ComponentValidator(_ =>
{
isFactoryCalled = true;
return validator;
});
await sut.ValidateAsync(componentObject, errors);
Assert.True(isFactoryCalled);
A.CallTo(() => validator.ValidateAsync(componentData, A<ValidationContext>._, A<AddError>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_do_nothing_if_value_is_not_a_component()
{
var isFactoryCalled = false;
var sut = new ComponentValidator(_ =>
{
isFactoryCalled = true;
return null!;
});
await sut.ValidateAsync(1, errors);
Assert.False(isFactoryCalled);
}
}
}

15
backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs

@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Rules.Json;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Domain.Apps.Core.Schemas.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Newtonsoft;
@ -81,8 +82,10 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
return new NewtonsoftJsonSerializer(serializerSettings); return new NewtonsoftJsonSerializer(serializerSettings);
} }
public static Schema MixedSchema(SchemaType type = SchemaType.Default) public static Schema MixedSchema(SchemaType type = SchemaType.Default, bool withMetadata = true)
{ {
var componentId = DomainId.NewGuid();
var schema = new Schema("user", type: type) var schema = new Schema("user", type: type)
.Publish() .Publish()
.AddArray(101, "root-array", Partitioning.Language, f => f .AddArray(101, "root-array", Partitioning.Language, f => f
@ -121,6 +124,10 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
new TagsFieldProperties()) new TagsFieldProperties())
.AddUI(113, "root-ui", Partitioning.Language, .AddUI(113, "root-ui", Partitioning.Language,
new UIFieldProperties()) new UIFieldProperties())
.AddComponent(114, "root-component", Partitioning.Language,
new ComponentFieldProperties { SchemaId = componentId })
.AddComponents(115, "root-components", Partitioning.Language,
new ComponentsFieldProperties { SchemaId = componentId })
.Update(new SchemaProperties { Hints = "The User" }) .Update(new SchemaProperties { Hints = "The User" })
.HideField(104) .HideField(104)
.HideField(211, 101) .HideField(211, 101)
@ -128,6 +135,12 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
.DisableField(212, 101) .DisableField(212, 101)
.LockField(105); .LockField(105);
if (withMetadata)
{
schema.FieldsById[114].SetResolvedSchema(componentId, schema);
schema.FieldsById[115].SetResolvedSchema(componentId, schema);
}
return schema; return schema;
} }

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs

@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
}; };
var schemaDef = var schemaDef =
new Schema("my-schema") new Schema("my-schema").Publish()
.AddNumber(1, "my-field1", Partitioning.Invariant, .AddNumber(1, "my-field1", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = true }) new NumberFieldProperties { IsRequired = true })
.AddNumber(2, "my-field2", Partitioning.Invariant, .AddNumber(2, "my-field2", Partitioning.Invariant,

44
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs

@ -29,17 +29,57 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
private readonly ISchemaEntity normalSchema; private readonly ISchemaEntity normalSchema;
private readonly ISchemaEntity normalUnpublishedSchema;
private readonly ISchemaEntity singletonSchema; private readonly ISchemaEntity singletonSchema;
private readonly ISchemaEntity singletonUnpublishedSchema;
private readonly ClaimsPrincipal user = Mocks.FrontendUser(); private readonly ClaimsPrincipal user = Mocks.FrontendUser();
private readonly RefToken actor = RefToken.User("123"); private readonly RefToken actor = RefToken.User("123");
public GuardContentTests() public GuardContentTests()
{ {
normalSchema = normalUnpublishedSchema =
Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); Mocks.Schema(appId, schemaId, new Schema(schemaId.Name));
singletonSchema = normalSchema =
Mocks.Schema(appId, schemaId, new Schema(schemaId.Name).Publish());
singletonUnpublishedSchema =
Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton)); Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton));
singletonSchema =
Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton).Publish());
}
[Fact]
public void Should_throw_exception_if_creating_content_for_unpublished_schema()
{
var context = CreateContext(CreateContent(Status.Draft), normalUnpublishedSchema);
Assert.Throws<DomainException>(() => context.MustNotCreateForUnpublishedSchema());
}
[Fact]
public void Should_not_throw_exception_if_creating_content_for_unpublished_singleton()
{
var context = CreateContext(CreateContent(Status.Draft, singletonSchema.Id), singletonUnpublishedSchema);
context.MustNotCreateSingleton();
}
[Fact]
public void Should_not_throw_exception_if_creating_content_for_published_schema()
{
var context = CreateContext(CreateContent(Status.Draft, singletonSchema.Id), normalSchema);
context.MustNotCreateSingleton();
}
[Fact]
public void Should_not_throw_exception_if_creating_content_for_published_singleton_schema()
{
var context = CreateContext(CreateContent(Status.Draft, singletonSchema.Id), singletonSchema);
context.MustNotCreateSingleton();
} }
[Fact] [Fact]

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

@ -41,6 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var schemaDef = var schemaDef =
new Schema(schemaId.Name) new Schema(schemaId.Name)
.Publish()
.SetScripts(new SchemaScripts { Query = "<query-script>" }); .SetScripts(new SchemaScripts { Query = "<query-script>" });
schema = Mocks.Schema(appId, schemaId, schemaDef); schema = Mocks.Schema(appId, schemaId, schemaDef);
@ -327,4 +328,4 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return content; return content;
} }
} }
} }

44
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ComponentsFieldPropertiesTests.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Validation;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties
{
public class ComponentsFieldPropertiesTests : IClassFixture<TranslationsFixture>
{
[Fact]
public void Should_add_error_if_min_items_greater_than_max_items()
{
var sut = new ComponentsFieldProperties { MinItems = 10, MaxItems = 5 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems")
});
}
[Fact]
public void Should_not_add_error_if_min_items_equals_to_max_items()
{
var sut = new ComponentsFieldProperties { MinItems = 2, MaxItems = 2 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
Assert.Empty(errors);
}
}
}

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

Loading…
Cancel
Save